From bfa0c36ccffe46ea1362e71bed08d9c4536f0512 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 15 May 2025 08:00:04 +0200 Subject: [PATCH] Cron jobs (#1220) * Flow fixes. * Temp * Remove duplicate timers. * Add dump. * Fix styles and UI bugs. * More and fixed tests * Fix tests * Fix e2e tests --- .../CreateContent/CreateContentFlowStep.cs | 1 - .../Actions/DeepDetect/DeepDetectFlowStep.cs | 1 - .../Actions/Discourse/DiscourseFlowStep.cs | 23 +- .../Actions/Fastly/FastlyFlowStep.cs | 14 +- .../Actions/Medium/MediumFlowStep.cs | 22 +- .../Actions/Prerender/PrerenderFlowStep.cs | 15 +- .../Squidex.Extensions/Actions/RuleHelper.cs | 49 - .../Actions/Slack/SlackFlowStep.cs | 15 +- .../Actions/Typesense/TypesenseFlowStep.cs | 15 +- .../Actions/Webhook/WebhookFlowStep.cs | 29 +- backend/i18n/frontend_en.json | 13 +- backend/i18n/frontend_fr.json | 11 + backend/i18n/frontend_it.json | 13 +- backend/i18n/frontend_nl.json | 11 + backend/i18n/frontend_pt.json | 11 + backend/i18n/frontend_zh.json | 13 +- backend/i18n/source/backend_en.json | 4 +- backend/i18n/source/backend_fr.json | 2 +- backend/i18n/source/backend_it.json | 2 +- backend/i18n/source/backend_nl.json | 2 +- backend/i18n/source/backend_pt.json | 2 +- backend/i18n/source/frontend_en.json | 13 +- .../AppDbContext.cs | 3 +- .../App/Migrations/20250504173108_AddFlows.cs | 3 +- .../20250513192106_AddCronJobs.Designer.cs | 1586 ++++++++++++++++ .../Migrations/20250513192106_AddCronJobs.cs | 43 + .../Migrations/MySqlDbContextModelSnapshot.cs | 20 + .../App/Migrations/20250504173116_AddFlows.cs | 3 +- .../20250513192113_AddCronJobs.Designer.cs | 1587 ++++++++++++++++ .../Migrations/20250513192113_AddCronJobs.cs | 40 + .../PostgresDbContextModelSnapshot.cs | 20 + .../App/Migrations/20250504173123_AddFlows.cs | 3 +- .../20250513192120_AddCronJobs.Designer.cs | 1589 +++++++++++++++++ .../Migrations/20250513192120_AddCronJobs.cs | 40 + .../SqlServerDbContextModelSnapshot.cs | 20 + .../ServiceExtensions.cs | 4 +- .../Squidex.Data.EntityFramework.csproj | 14 +- .../Squidex.Data.MongoDb/ServiceExtensions.cs | 6 +- .../Squidex.Data.MongoDb.csproj | 12 +- .../EnrichedEvents/EnrichedCronJobEvent.cs | 20 + .../Rules/IRuleTriggerVisitor.cs | 2 + .../Rules/Triggers/CronJobTrigger.cs | 26 + .../Squidex.Domain.Apps.Core.Model.csproj | 2 +- .../HandleRules/CronJobContext.cs | 14 + ...Squidex.Domain.Apps.Core.Operations.csproj | 4 +- .../Rules/CronJobTriggerHandler.cs | 44 + .../Rules/CronJobUpdater.cs | 90 + .../Rules/DomainObject/Guards/GuardRule.cs | 3 +- .../DomainObject/RuleDomainObject.State.cs | 1 - .../Rules/DomainObject/RuleDomainObject.cs | 4 +- .../Rules/IRuleEnqueuer.cs | 3 +- .../Rules/RuleEnqueuer.cs | 15 +- .../Rules/RuleJobUpdate.cs | 26 - .../Rules/RuleValidator.cs | 27 +- .../Rules/Runner/DefaultRuleRunnerService.cs | 96 +- .../Rules/Runner/IRuleRunnerService.cs | 3 +- .../Rules/RuleCronJobTriggered.cs} | 12 +- .../Commands/DomainObject.cs | 2 +- .../Http/DumpFormatter.cs | 105 -- .../Queries/OData/FilterVisitor.cs | 1 - .../Squidex.Infrastructure.csproj | 14 +- backend/src/Squidex.Shared/Texts.fr.resx | 12 +- backend/src/Squidex.Shared/Texts.it.resx | 12 +- backend/src/Squidex.Shared/Texts.nl.resx | 12 +- backend/src/Squidex.Shared/Texts.pt.resx | 12 +- backend/src/Squidex.Shared/Texts.resx | 12 +- backend/src/Squidex.Shared/Texts.zh.resx | 12 +- .../Config/OpenApi/DiscriminatorProcessor.cs | 1 - .../Controllers/Assets/AssetsController.cs | 11 +- .../Converters/RuleTriggerDtoFactory.cs | 5 + .../Controllers/Rules/Models/CreateRuleDto.cs | 10 +- .../Rules/Models/DynamicUpdateRuleDto.cs | 1 - .../Models/Triggers/CronJobRuleTriggerDto.cs | 43 + .../Api/Controllers/Rules/RulesController.cs | 34 +- .../Controllers/Schemas/SchemasController.cs | 27 +- .../src/Squidex/Config/Domain/RuleServices.cs | 10 + .../Config/Messaging/MessagingServices.cs | 4 +- backend/src/Squidex/Squidex.csproj | 24 +- .../CodeGenerator.cs | 1 - .../EventJsonSchemaGeneratorTests.cs | 1 + .../Rules/CronJobTriggerHandlerTests.cs | 79 + .../Rules/CronJobUpdaterTests.cs | 221 +++ .../DomainObject/Guards/GuardRuleTests.cs | 10 +- .../Triggers/ContentChangedTriggerTests.cs | 2 +- .../Triggers/CronJobTriggerValidationTests.cs | 89 + .../Triggers/UsageTriggerValidationTests.cs | 2 +- .../DomainObject/RuleDomainObjectTests.cs | 6 +- .../Rules/RuleEnqueuerTests.cs | 22 +- .../Rules/RuleFlowTrackingCallbackTests.cs | 80 + .../Rules/RuleValidatorTests.cs | 4 +- .../TestHelpers/GivenContext.cs | 9 +- .../Http/DumpFormatterTests.cs | 127 -- .../pages/users/user-page.component.html | 6 +- .../rules/pages/events/rule-event.stories.ts | 96 +- .../pages/rule/step-dialog.component.html | 18 +- .../pages/rule/trigger-dialog.component.html | 15 +- .../pages/rule/trigger-dialog.component.ts | 2 + .../rules/pages/rules/rule.component.html | 8 +- .../simulated-rule-event.component.html | 4 +- .../simulator/simulated-rule-event.stories.ts | 2 - .../rules/shared/history-step.component.html | 13 +- .../rules/shared/history-step.component.ts | 11 +- .../rules/shared/state-details.component.html | 3 +- .../shared/state-step-property.component.html | 12 + .../shared/state-step-property.component.scss | 6 + .../shared/state-step-property.component.ts | 63 + .../rules/shared/state-step.component.html | 22 +- .../rules/shared/state-step.component.scss | 4 + .../rules/shared/state-step.component.ts | 15 +- .../asset-changed-trigger.component.html | 46 +- .../triggers/comment-trigger.component.html | 38 +- .../triggers/cron-job-trigger.component.html | 56 + .../triggers/cron-job-trigger.component.scss | 0 .../triggers/cron-job-trigger.component.ts | 39 + .../schema-changed-trigger.component.html | 38 +- .../triggers/usage-trigger.component.html | 25 +- .../common/schema-edit-form.component.html | 18 +- .../pages/more/more-page.component.html | 3 +- .../teams/pages/more/more-page.component.html | 3 +- .../forms/editors/code-editor.component.html | 2 +- .../forms/editors/code-editor.component.scss | 8 +- .../assets/asset-dialog.component.html | 13 +- frontend/src/app/shared/model/generated.ts | 760 +++++++- .../app/shared/services/rules.service.spec.ts | 17 + .../src/app/shared/services/rules.service.ts | 14 + frontend/src/app/shared/state/rules.forms.ts | 10 + tools/e2e/tests/given-app/rules.spec.ts | 6 +- tools/e2e/tests/pages/rule.ts | 8 + 128 files changed, 7405 insertions(+), 682 deletions(-) create mode 100644 backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.Designer.cs create mode 100644 backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.cs create mode 100644 backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.Designer.cs create mode 100644 backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.cs create mode 100644 backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.Designer.cs create mode 100644 backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCronJobEvent.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CronJobTrigger.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/CronJobContext.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/CronJobTriggerHandler.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/CronJobUpdater.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobUpdate.cs rename backend/src/{Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs => Squidex.Domain.Apps.Events/Rules/RuleCronJobTriggered.cs} (67%) delete mode 100644 backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/CronJobRuleTriggerDto.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/CronJobTriggerHandlerTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/CronJobUpdaterTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/CronJobTriggerValidationTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleFlowTrackingCallbackTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs create mode 100644 frontend/src/app/features/rules/shared/state-step-property.component.html create mode 100644 frontend/src/app/features/rules/shared/state-step-property.component.scss create mode 100644 frontend/src/app/features/rules/shared/state-step-property.component.ts create mode 100644 frontend/src/app/features/rules/shared/triggers/cron-job-trigger.component.html create mode 100644 frontend/src/app/features/rules/shared/triggers/cron-job-trigger.component.scss create mode 100644 frontend/src/app/features/rules/shared/triggers/cron-job-trigger.component.ts diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentFlowStep.cs index 828e75487..5e328349a 100644 --- a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentFlowStep.cs @@ -15,7 +15,6 @@ using Squidex.Domain.Apps.Entities; using Squidex.Flows; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; using Command = Squidex.Domain.Apps.Entities.Contents.Commands.CreateContent; diff --git a/backend/extensions/Squidex.Extensions/Actions/DeepDetect/DeepDetectFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/DeepDetect/DeepDetectFlowStep.cs index 3ed12a04a..c65968fe7 100644 --- a/backend/extensions/Squidex.Extensions/Actions/DeepDetect/DeepDetectFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/DeepDetect/DeepDetectFlowStep.cs @@ -8,7 +8,6 @@ using System.ComponentModel.DataAnnotations; using System.Net.Http.Json; using System.Text.RegularExpressions; -using Google.Apis.Json; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.HandleRules; diff --git a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseFlowStep.cs index a6a083b95..50b09f021 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseFlowStep.cs @@ -10,7 +10,7 @@ using System.Text; using Squidex.Domain.Apps.Core.Rules.Deprecated; using Squidex.Extensions.Actions.Algolia; using Squidex.Flows; -using Squidex.Infrastructure.Json; +using Squidex.Flows.Steps.Utils; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; @@ -65,12 +65,6 @@ public sealed record DiscourseFlowStep : FlowStep, IConvertibleToAction public override async ValueTask ExecuteAsync(FlowExecutionContext executionContext, CancellationToken ct) { - if (executionContext.IsSimulation) - { - executionContext.LogSkipSimulation(); - return Next(); - } - var url = $"{Url.ToString().TrimEnd('/')}/posts.json?api_key={ApiKey}&api_username={ApiUsername}"; var body = new Dictionary @@ -92,10 +86,6 @@ public sealed record DiscourseFlowStep : FlowStep, IConvertibleToAction var jsonRequest = executionContext.SerializeJson(body); - var httpClient = - executionContext.Resolve() - .CreateClient("DiscourseAction"); - var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"), @@ -104,6 +94,17 @@ public sealed record DiscourseFlowStep : FlowStep, IConvertibleToAction request.Headers.TryAddWithoutValidation("Api-Key", ApiKey); request.Headers.TryAddWithoutValidation("Api-Username", ApiUsername); + if (executionContext.IsSimulation) + { + executionContext.LogSkipSimulation( + HttpDumpFormatter.BuildDump(request, null, null)); + return Next(); + } + + var httpClient = + executionContext.Resolve() + .CreateClient("DiscourseAction"); + var (_, dump) = await httpClient.SendAsync(executionContext, request, jsonRequest, ct); if (Topic != null) diff --git a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyFlowStep.cs index 73e6c8765..e3b1ca54b 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyFlowStep.cs @@ -10,6 +10,7 @@ using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.Deprecated; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Flows; +using Squidex.Flows.Steps.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; @@ -40,12 +41,6 @@ public sealed record FastlyFlowStep : FlowStep, IConvertibleToAction public override async ValueTask ExecuteAsync(FlowExecutionContext executionContext, CancellationToken ct) { - if (executionContext.IsSimulation) - { - executionContext.LogSkipSimulation(); - return Next(); - } - var @event = ((FlowEventContext)executionContext.Context).Event; var id = string.Empty; @@ -61,6 +56,13 @@ public sealed record FastlyFlowStep : FlowStep, IConvertibleToAction var requestUrl = $"/service/{ServiceId}/purge/{id}"; var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); + if (executionContext.IsSimulation) + { + executionContext.LogSkipSimulation( + HttpDumpFormatter.BuildDump(request, null, null)); + return Next(); + } + request.Headers.Add("Fastly-Key", ApiKey); var (_, dump) = await httpClient.SendAsync(executionContext, request, ct: ct); diff --git a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumFlowStep.cs index b7618fdb7..f0f455945 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumFlowStep.cs @@ -9,7 +9,7 @@ using System.ComponentModel.DataAnnotations; using System.Text; using Squidex.Domain.Apps.Core.Rules.Deprecated; using Squidex.Flows; -using Squidex.Infrastructure.Json; +using Squidex.Flows.Steps.Utils; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; @@ -64,12 +64,6 @@ public sealed record MediumFlowStep : FlowStep, IConvertibleToAction public override async ValueTask ExecuteAsync(FlowExecutionContext executionContext, CancellationToken ct) { - if (executionContext.IsSimulation) - { - executionContext.LogSkipSimulation(); - return Next(); - } - var httpClient = executionContext.Resolve() .CreateClient("MediumAction"); @@ -79,6 +73,10 @@ public sealed record MediumFlowStep : FlowStep, IConvertibleToAction { path = $"/v1/publications/{PublicationId}/posts"; } + else if (executionContext.IsSimulation) + { + path = "/v1/users/simulated-id/posts"; + } else { var meRequest = BuildGetRequest("/v1/me"); @@ -101,8 +99,16 @@ public sealed record MediumFlowStep : FlowStep, IConvertibleToAction }; var requestJson = executionContext.SerializeJson(requestBody); + var requestMessage = BuildPostRequest(path, requestJson); + + if (executionContext.IsSimulation) + { + executionContext.LogSkipSimulation( + HttpDumpFormatter.BuildDump(requestMessage, null, null)); + return Next(); + } - var (_, dump) = await httpClient.SendAsync(executionContext, BuildPostRequest(path, requestJson), requestJson, ct); + var (_, dump) = await httpClient.SendAsync(executionContext, requestMessage, requestJson, ct); executionContext.Log("Post created", dump); return Next(); diff --git a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderFlowStep.cs index ccacda89b..869e6f516 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderFlowStep.cs @@ -9,7 +9,7 @@ using System.ComponentModel.DataAnnotations; using System.Text; using Squidex.Domain.Apps.Core.Rules.Deprecated; using Squidex.Flows; -using Squidex.Infrastructure.Json; +using Squidex.Flows.Steps.Utils; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; @@ -41,12 +41,6 @@ public sealed record PrerenderFlowStep : FlowStep, IConvertibleToAction public override async ValueTask ExecuteAsync(FlowExecutionContext executionContext, CancellationToken ct) { - if (executionContext.IsSimulation) - { - executionContext.LogSkipSimulation(); - return Next(); - } - var requestObject = new { prerenderToken = Token, Url }; var requestBody = executionContext.SerializeJson(requestObject); @@ -55,6 +49,13 @@ public sealed record PrerenderFlowStep : FlowStep, IConvertibleToAction Content = new StringContent(requestBody, Encoding.UTF8, "application/json"), }; + if (executionContext.IsSimulation) + { + executionContext.LogSkipSimulation( + HttpDumpFormatter.BuildDump(request, null, null)); + return Next(); + } + var httpClient = executionContext.Resolve() .CreateClient("Prerender"); diff --git a/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs b/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs index e9eafe24a..6cfb9af45 100644 --- a/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs +++ b/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs @@ -6,31 +6,13 @@ // ========================================================================== using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Scripting; using Squidex.Flows; using Squidex.Infrastructure; -using Squidex.Infrastructure.Http; namespace Squidex.Extensions.Actions; public static class RuleHelper { - public static bool ShouldDelete(this EnrichedEvent @event, IScriptEngine scriptEngine, string? expression) - { - if (!string.IsNullOrWhiteSpace(expression)) - { - // Script vars are just wrappers over dictionaries for better performance. - var vars = new EventScriptVars - { - Event = @event, - }; - - return scriptEngine.Evaluate(vars, expression); - } - - return IsContentDeletion(@event) || IsAssetDeletion(@event); - } - public static bool ShouldDelete(this EnrichedEvent @event, FlowExecutionContext context, string? expression) { if (!string.IsNullOrWhiteSpace(expression)) @@ -51,37 +33,6 @@ public static class RuleHelper return @event is EnrichedAssetEvent { Type: EnrichedAssetEventType.Deleted }; } - public static async Task<(string Response, string Dump)> SendAsync(this HttpClient client, - FlowExecutionContext executionContext, - HttpRequestMessage request, - string? requestBody = null, - CancellationToken ct = default) - { - HttpResponseMessage? response = null; - try - { - response = await client.SendAsync(request, ct); - - var responseString = await response.Content.ReadAsStringAsync(ct); - var requestDump = DumpFormatter.BuildDump(request, response, requestBody, responseString); - - if (!response.IsSuccessStatusCode) - { - executionContext.Log("Http request failed", requestDump); - throw new HttpRequestException($"Response code does not indicate success: {(int)response.StatusCode} ({response.StatusCode})."); - } - - return (responseString, requestDump); - } - catch (Exception ex) - { - var requestDump = DumpFormatter.BuildDump(request, response, requestBody, ex.ToString()); - - executionContext.Log("Http request failed", requestDump); - throw; - } - } - public static (string Id, bool IsGenerated) GetOrCreateId(this EnrichedEvent @event) { if (@event is IEnrichedEntityEvent enrichedEntityEvent) diff --git a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackFlowStep.cs index 1a5dfdbf0..7123c38e9 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackFlowStep.cs @@ -7,10 +7,10 @@ using System.ComponentModel.DataAnnotations; using System.Text; -using Google.Apis.Json; using Migrations.OldActions; using Squidex.Domain.Apps.Core.Rules.Deprecated; using Squidex.Flows; +using Squidex.Flows.Steps.Utils; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; @@ -42,12 +42,6 @@ public sealed record SlackFlowStep : FlowStep, IConvertibleToAction public override async ValueTask ExecuteAsync(FlowExecutionContext executionContext, CancellationToken ct) { - if (executionContext.IsSimulation) - { - executionContext.LogSkipSimulation(); - return Next(); - } - var body = new { text = Text }; var jsonRequest = executionContext.SerializeJson(body); @@ -57,6 +51,13 @@ public sealed record SlackFlowStep : FlowStep, IConvertibleToAction Content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"), }; + if (executionContext.IsSimulation) + { + executionContext.LogSkipSimulation( + HttpDumpFormatter.BuildDump(request, null, null)); + return Next(); + } + var httpClient = executionContext.Resolve() .CreateClient("SlackAction"); diff --git a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseFlowStep.cs index 40b1933b7..e97c0502a 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseFlowStep.cs @@ -11,7 +11,7 @@ using System.Text.Json.Serialization; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.Deprecated; using Squidex.Flows; -using Squidex.Infrastructure.Json; +using Squidex.Flows.Steps.Utils; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; @@ -100,16 +100,17 @@ public sealed record TypesenseFlowStep : FlowStep, IConvertibleToAction return Next(); } - if (executionContext.IsSimulation) - { - executionContext.LogSkipSimulation(); - return Next(); - } - async Task SendAsync(HttpRequestMessage request, string? body, string message) { request.Headers.TryAddWithoutValidation("X-Typesense-Api-Key", ApiKey); + if (executionContext.IsSimulation) + { + executionContext.LogSkipSimulation( + HttpDumpFormatter.BuildDump(request, null, null)); + return; + } + var httpClient = executionContext.Resolve() .CreateClient("TypesenseAction"); diff --git a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookFlowStep.cs index 6cb4319ea..7f616476c 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookFlowStep.cs @@ -9,7 +9,7 @@ using System.ComponentModel.DataAnnotations; using System.Text; using Squidex.Domain.Apps.Core.Rules.Deprecated; using Squidex.Flows; -using Squidex.Flows.Steps; +using Squidex.Flows.Steps.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; @@ -74,13 +74,7 @@ public sealed record WebhookFlowStep : FlowStep, IConvertibleToAction break; } - if (executionContext.IsSimulation) - { - executionContext.LogSkipSimulation(); - return Next(); - } - - var httpClient = executionContext.Resolve().CreateClient("FlowClient"); + string? requestBody = null; var request = new HttpRequestMessage(method, Url); if (!string.IsNullOrEmpty(Payload) && Method != WebhookMethod.GET) @@ -91,6 +85,7 @@ public sealed record WebhookFlowStep : FlowStep, IConvertibleToAction mediaType = "application/json"; } + requestBody = Payload; request.Content = new StringContent(Payload, Encoding.UTF8, mediaType); } @@ -103,11 +98,23 @@ public sealed record WebhookFlowStep : FlowStep, IConvertibleToAction } } - var signature = $"{Payload}{SharedSecret}".ToSha256Base64(); + if (!string.IsNullOrWhiteSpace(SharedSecret)) + { + var signature = $"{Payload}{SharedSecret}".ToSha256Base64(); - request.Headers.Add("X-Signature", signature); + request.Headers.Add("X-Signature", signature); + } + + if (executionContext.IsSimulation) + { + executionContext.LogSkipSimulation( + HttpDumpFormatter.BuildDump(request, null, requestBody, null)); + return Next(); + } + + var httpClient = executionContext.Resolve().CreateClient("FlowClient"); - var (_, dump) = await httpClient.SendAsync(executionContext, request, Payload, ct); + var (_, dump) = await httpClient.SendAsync(executionContext, request, requestBody, ct); executionContext.Log("HTTP request sent", dump); return Next(); diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index adc18f4ab..423e6cb84 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -336,6 +336,7 @@ "common.prevPage": "Previous Page", "common.product": "Squidex Headless CMS", "common.project": "Project", + "common.properties": "Properties", "common.queryOperators.contains": "contains", "common.queryOperators.empty": "is empty", "common.queryOperators.endsWith": "ends with", @@ -714,6 +715,15 @@ "rules.conditions.commentKeyword": "Only for text keywords", "rules.conditions.commentUser": "Specific users", "rules.conditions.contentValue": "Content has value", + "rules.conditions.cronExpression": "Cron Expression", + "rules.conditions.cronExpressionEvery4Hours": "Every 4 hours", + "rules.conditions.cronExpressionEveryMonth": "First day every month, at 6 AM in the morning", + "rules.conditions.cronExpressionEveryMorning": "Every day at 6 AM in the morning", + "rules.conditions.cronExpressionHint": "A cron expression is a string consisting of six or seven subexpressions (fields) that describe individual details of the schedule. You cannot create expressions that trigger this flow more often than every 4 hours.", + "rules.conditions.cronExpressionsHint": "Typical cron expressions", + "rules.conditions.cronExpressionsTitle": "Cron Expressions", + "rules.conditions.cronTimezone": "Timezone", + "rules.conditions.cronTimezoneHint": "The timezone as IANA format to define how to calculate the next time the flow should be run. Use your local timezone if a job should run every morning before working hours.", "rules.conditions.event": "Specific events", "rules.conditions.images": "Images only", "rules.conditions.largeAssets": "Large assets", @@ -761,6 +771,7 @@ "rules.ruleEvents.loadFailed": "Failed to load events. Please reload.", "rules.ruleEvents.nextAttemptLabel": "Next", "rules.ruleEvents.reloaded": "RuleEvents reloaded.", + "rules.ruleEvents.validationFailed": "Failed to validate rule.", "rules.ruleSimulator.listPageTitle": "Simulator", "rules.ruleSyntax.if": "when", "rules.ruleSyntax.then": "then", @@ -783,7 +794,7 @@ "rules.simulation.errorTooOld": "STOP: Event is too old.", "rules.simulation.errorWrongEvent": "STOP: Event does not match to the trigger.", "rules.simulation.errorWrongEventForTrigger": "STOP: Event does not match to the trigger.", - "rules.simulation.eventConditionEvaluated": "Enriched event is evaluated, whether it matchs to the conditions and javascript expressions in the trigger.", + "rules.simulation.eventConditionEvaluated": "Enriched is tested against the conditions and javascript expressions in the trigger.", "rules.simulation.eventEnriched": "Event is enriched with additional data", "rules.simulation.eventQueried": "Event is queried from the database", "rules.simulation.eventTriggerChecked": "Event is tested to see if it matchs to the trigger and the basic conditions.", diff --git a/backend/i18n/frontend_fr.json b/backend/i18n/frontend_fr.json index 57399ad11..604a65fd8 100644 --- a/backend/i18n/frontend_fr.json +++ b/backend/i18n/frontend_fr.json @@ -336,6 +336,7 @@ "common.prevPage": "Previous Page", "common.product": "CMS sans tête Squidex", "common.project": "Projet", + "common.properties": "Properties", "common.queryOperators.contains": "contient", "common.queryOperators.empty": "est vide", "common.queryOperators.endsWith": "se termine par", @@ -714,6 +715,15 @@ "rules.conditions.commentKeyword": "Uniquement pour les mots-clés textuels", "rules.conditions.commentUser": "Utilisateurs spécifiques", "rules.conditions.contentValue": "Le contenu a de la valeur", + "rules.conditions.cronExpression": "Cron Expression", + "rules.conditions.cronExpressionEvery4Hours": "Every 4 hours", + "rules.conditions.cronExpressionEveryMonth": "First day every month, at 6 AM in the morning", + "rules.conditions.cronExpressionEveryMorning": "Every day at 6 AM in the morning", + "rules.conditions.cronExpressionHint": "A cron expression is a string consisting of six or seven subexpressions (fields) that describe individual details of the schedule. You cannot create expressions that trigger this flow more often than every 4 hours.", + "rules.conditions.cronExpressionsHint": "Typical cron expressions", + "rules.conditions.cronExpressionsTitle": "Cron Expressions", + "rules.conditions.cronTimezone": "Timezone", + "rules.conditions.cronTimezoneHint": "The timezone as IANA format to define how to calculate the next time the flow should be run. Use your local timezone if a job should run every morning before working hours.", "rules.conditions.event": "Événements spécifiques", "rules.conditions.images": "Images uniquement", "rules.conditions.largeAssets": "Grands atouts", @@ -761,6 +771,7 @@ "rules.ruleEvents.loadFailed": "Échec du chargement des événements. Veuillez recharger.", "rules.ruleEvents.nextAttemptLabel": "Suivant", "rules.ruleEvents.reloaded": "RuleEvents rechargé.", + "rules.ruleEvents.validationFailed": "Failed to validate rule.", "rules.ruleSimulator.listPageTitle": "Simulateur", "rules.ruleSyntax.if": "Si", "rules.ruleSyntax.then": "alors", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index ffea03a47..46c1b12ec 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -336,6 +336,7 @@ "common.prevPage": "Previous Page", "common.product": "Squidex Headless CMS", "common.project": "Progetto", + "common.properties": "Properties", "common.queryOperators.contains": "contiene", "common.queryOperators.empty": "è vuoto", "common.queryOperators.endsWith": "finisce con", @@ -714,6 +715,15 @@ "rules.conditions.commentKeyword": "Only for text keywords", "rules.conditions.commentUser": "Specific users", "rules.conditions.contentValue": "Content has value", + "rules.conditions.cronExpression": "Cron Expression", + "rules.conditions.cronExpressionEvery4Hours": "Every 4 hours", + "rules.conditions.cronExpressionEveryMonth": "First day every month, at 6 AM in the morning", + "rules.conditions.cronExpressionEveryMorning": "Every day at 6 AM in the morning", + "rules.conditions.cronExpressionHint": "A cron expression is a string consisting of six or seven subexpressions (fields) that describe individual details of the schedule. You cannot create expressions that trigger this flow more often than every 4 hours.", + "rules.conditions.cronExpressionsHint": "Typical cron expressions", + "rules.conditions.cronExpressionsTitle": "Cron Expressions", + "rules.conditions.cronTimezone": "Timezone", + "rules.conditions.cronTimezoneHint": "The timezone as IANA format to define how to calculate the next time the flow should be run. Use your local timezone if a job should run every morning before working hours.", "rules.conditions.event": "Specific events", "rules.conditions.images": "Images only", "rules.conditions.largeAssets": "Large assets", @@ -761,6 +771,7 @@ "rules.ruleEvents.loadFailed": "Non è stato possibile caricare gli eventi. Per favore ricarica.", "rules.ruleEvents.nextAttemptLabel": "Successivo", "rules.ruleEvents.reloaded": "Eventi della regola ricaricati.", + "rules.ruleEvents.validationFailed": "Failed to validate rule.", "rules.ruleSimulator.listPageTitle": "Simulator", "rules.ruleSyntax.if": "Se", "rules.ruleSyntax.then": "Allora", @@ -783,7 +794,7 @@ "rules.simulation.errorTooOld": "STOP: Event is too old.", "rules.simulation.errorWrongEvent": "STOP: Event does not match to the trigger.", "rules.simulation.errorWrongEventForTrigger": "STOP: Event does not match to the trigger.", - "rules.simulation.eventConditionEvaluated": "Enriched event is evaluated, whether it matchs to the conditions and javascript expressions in the trigger.", + "rules.simulation.eventConditionEvaluated": "Enriched is tested against the conditions and javascript expressions in the trigger.", "rules.simulation.eventEnriched": "Event is enriched with additional data", "rules.simulation.eventQueried": "Event is queried from the database", "rules.simulation.eventTriggerChecked": "Event is tested to see if it matchs to the trigger and the basic conditions.", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 537972bb7..bab57073b 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -336,6 +336,7 @@ "common.prevPage": "Previous Page", "common.product": "Squidex Headless CMS", "common.project": "Project", + "common.properties": "Properties", "common.queryOperators.contains": "bevat", "common.queryOperators.empty": "is leeg", "common.queryOperators.endsWith": "eindigt op", @@ -714,6 +715,15 @@ "rules.conditions.commentKeyword": "Only for text keywords", "rules.conditions.commentUser": "Specific users", "rules.conditions.contentValue": "Content has value", + "rules.conditions.cronExpression": "Cron Expression", + "rules.conditions.cronExpressionEvery4Hours": "Every 4 hours", + "rules.conditions.cronExpressionEveryMonth": "First day every month, at 6 AM in the morning", + "rules.conditions.cronExpressionEveryMorning": "Every day at 6 AM in the morning", + "rules.conditions.cronExpressionHint": "A cron expression is a string consisting of six or seven subexpressions (fields) that describe individual details of the schedule. You cannot create expressions that trigger this flow more often than every 4 hours.", + "rules.conditions.cronExpressionsHint": "Typical cron expressions", + "rules.conditions.cronExpressionsTitle": "Cron Expressions", + "rules.conditions.cronTimezone": "Timezone", + "rules.conditions.cronTimezoneHint": "The timezone as IANA format to define how to calculate the next time the flow should be run. Use your local timezone if a job should run every morning before working hours.", "rules.conditions.event": "Specific events", "rules.conditions.images": "Images only", "rules.conditions.largeAssets": "Large assets", @@ -761,6 +771,7 @@ "rules.ruleEvents.loadFailed": "Kan evenementen niet laden. Laad opnieuw.", "rules.ruleEvents.nextAttemptLabel": "Volgende", "rules.ruleEvents.reloaded": "RuleEvents herladen.", + "rules.ruleEvents.validationFailed": "Failed to validate rule.", "rules.ruleSimulator.listPageTitle": "Simulator", "rules.ruleSyntax.if": "If", "rules.ruleSyntax.then": "then", diff --git a/backend/i18n/frontend_pt.json b/backend/i18n/frontend_pt.json index fc94fe065..30c4a8d8d 100644 --- a/backend/i18n/frontend_pt.json +++ b/backend/i18n/frontend_pt.json @@ -336,6 +336,7 @@ "common.prevPage": "Previous Page", "common.product": "CMS Headless Squidex", "common.project": "Projeto", + "common.properties": "Properties", "common.queryOperators.contains": "contém", "common.queryOperators.empty": "está vazio", "common.queryOperators.endsWith": "termina com", @@ -714,6 +715,15 @@ "rules.conditions.commentKeyword": "Apenas para palavras-chave de texto", "rules.conditions.commentUser": "Utilizadores específicos", "rules.conditions.contentValue": "O conteúdo tem valor", + "rules.conditions.cronExpression": "Cron Expression", + "rules.conditions.cronExpressionEvery4Hours": "Every 4 hours", + "rules.conditions.cronExpressionEveryMonth": "First day every month, at 6 AM in the morning", + "rules.conditions.cronExpressionEveryMorning": "Every day at 6 AM in the morning", + "rules.conditions.cronExpressionHint": "A cron expression is a string consisting of six or seven subexpressions (fields) that describe individual details of the schedule. You cannot create expressions that trigger this flow more often than every 4 hours.", + "rules.conditions.cronExpressionsHint": "Typical cron expressions", + "rules.conditions.cronExpressionsTitle": "Cron Expressions", + "rules.conditions.cronTimezone": "Timezone", + "rules.conditions.cronTimezoneHint": "The timezone as IANA format to define how to calculate the next time the flow should be run. Use your local timezone if a job should run every morning before working hours.", "rules.conditions.event": "Eventos específicos", "rules.conditions.images": "Apenas imagens", "rules.conditions.largeAssets": "Grandes ativos", @@ -761,6 +771,7 @@ "rules.ruleEvents.loadFailed": "Falhou em carregar eventos. Por favor, recarregue.", "rules.ruleEvents.nextAttemptLabel": "A seguir", "rules.ruleEvents.reloaded": "RegrasEventos recarregados.", + "rules.ruleEvents.validationFailed": "Failed to validate rule.", "rules.ruleSimulator.listPageTitle": "Simulador", "rules.ruleSyntax.if": "If", "rules.ruleSyntax.then": "then", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 4edc40874..86664426e 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -336,6 +336,7 @@ "common.prevPage": "Previous Page", "common.product": "Squidex Headless CMS", "common.project": "项目", + "common.properties": "Properties", "common.queryOperators.contains": "包含", "common.queryOperators.empty": "为空", "common.queryOperators.endsWith": "以", @@ -714,6 +715,15 @@ "rules.conditions.commentKeyword": "Only for text keywords", "rules.conditions.commentUser": "Specific users", "rules.conditions.contentValue": "Content has value", + "rules.conditions.cronExpression": "Cron Expression", + "rules.conditions.cronExpressionEvery4Hours": "Every 4 hours", + "rules.conditions.cronExpressionEveryMonth": "First day every month, at 6 AM in the morning", + "rules.conditions.cronExpressionEveryMorning": "Every day at 6 AM in the morning", + "rules.conditions.cronExpressionHint": "A cron expression is a string consisting of six or seven subexpressions (fields) that describe individual details of the schedule. You cannot create expressions that trigger this flow more often than every 4 hours.", + "rules.conditions.cronExpressionsHint": "Typical cron expressions", + "rules.conditions.cronExpressionsTitle": "Cron Expressions", + "rules.conditions.cronTimezone": "Timezone", + "rules.conditions.cronTimezoneHint": "The timezone as IANA format to define how to calculate the next time the flow should be run. Use your local timezone if a job should run every morning before working hours.", "rules.conditions.event": "Specific events", "rules.conditions.images": "Images only", "rules.conditions.largeAssets": "Large assets", @@ -761,6 +771,7 @@ "rules.ruleEvents.loadFailed": "加载事件失败。请重新加载。", "rules.ruleEvents.nextAttemptLabel": "下一个", "rules.ruleEvents.reloaded": "RuleEvents 重新加载。", + "rules.ruleEvents.validationFailed": "Failed to validate rule.", "rules.ruleSimulator.listPageTitle": "模拟器", "rules.ruleSyntax.if": "如果", "rules.ruleSyntax.then": "那么", @@ -783,7 +794,7 @@ "rules.simulation.errorTooOld": "STOP: Event is too old.", "rules.simulation.errorWrongEvent": "STOP: Event does not match to the trigger.", "rules.simulation.errorWrongEventForTrigger": "STOP: Event does not match to the trigger.", - "rules.simulation.eventConditionEvaluated": "Enriched event is evaluated, whether it matchs to the conditions and javascript expressions in the trigger.", + "rules.simulation.eventConditionEvaluated": "Enriched is tested against the conditions and javascript expressions in the trigger.", "rules.simulation.eventEnriched": "Event is enriched with additional data", "rules.simulation.eventQueried": "Event is queried from the database", "rules.simulation.eventTriggerChecked": "Event is tested to see if it matchs to the trigger and the basic conditions.", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 41fa63b2e..2fe628c7a 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -280,10 +280,13 @@ "login.test.headline": "Successful Test", "login.test.text": "Login Test was successful. You can save the settings now. It is recommended to make a final test with the actual login flow.", "login.test.title": "Login Test", + "rules.validation.invalidCronExpression": "Cron Expression is invalid.", + "rules.validation.invalidCronTimezone": "Timezone is not a valid IANA identifier.", "rules.validation.invalidNextStepId": "Invalid next step ID", "rules.validation.invalidStepId": "Invalid step ID", "rules.validation.noStartStep": "Flow has no start step", "rules.validation.noSteps": "Flow has no step", + "rules.validation.schemaNotFound": "Schema {id} does not exist.", "schemas.dateTimeCalculatedDefaultAndDefaultError": "Calculated default value and default value cannot be used together.", "schemas.duplicateFieldName": "Field '{field}' has been added twice.", "schemas.fieldCannotBeUIField": "Field cannot be an UI field.", @@ -293,7 +296,6 @@ "schemas.fieldsNotCovered": "Field ids do not cover all fields.", "schemas.nameAlreadyExists": "A schema with the same name already exists.", "schemas.noPermission": "You do not have permission for this schema.", - "schemas.notFoundId": "Schema {id} does not exist.", "schemas.number.inlineEditorError": "Inline editing is not allowed for Radio editor.", "schemas.onlyArraysHaveNested": "Only array fields can have nested fields.", "schemas.onylArraysInRoot": "Nested field cannot be array fields.", diff --git a/backend/i18n/source/backend_fr.json b/backend/i18n/source/backend_fr.json index 93ce1d5f8..0568aae4b 100644 --- a/backend/i18n/source/backend_fr.json +++ b/backend/i18n/source/backend_fr.json @@ -261,6 +261,7 @@ "history.teams.planReset": "plan réinitialisé.", "history.teams.updated": "paramètres généraux mis à jour et nom renommé en {[Name]}.", "login.githubPrivateEmail": "Votre adresse e-mail est définie sur privé dans Github. Veuillez le définir sur public pour utiliser la connexion Github.", + "rules.validation.schemaNotFound": "Le schéma {id} n'existe pas.", "schemas.dateTimeCalculatedDefaultAndDefaultError": "La valeur par défaut calculée et la valeur par défaut ne peuvent pas être utilisées ensemble.", "schemas.duplicateFieldName": "Le champ '{field}' a été ajouté deux fois.", "schemas.fieldCannotBeUIField": "Le champ ne peut pas être un champ d'interface utilisateur.", @@ -270,7 +271,6 @@ "schemas.fieldsNotCovered": "Les ID de champ ne couvrent pas tous les champs.", "schemas.nameAlreadyExists": "Un schéma portant le même nom existe déjà.", "schemas.noPermission": "Vous n'avez pas l'autorisation pour ce schéma.", - "schemas.notFoundId": "Le schéma {id} n'existe pas.", "schemas.number.inlineEditorError": "L'édition en ligne n'est pas autorisée pour l'éditeur radio.", "schemas.onlyArraysHaveNested": "Seuls les champs de tableau peuvent avoir des champs imbriqués.", "schemas.onylArraysInRoot": "Les champs imbriqués ne peuvent pas être des champs de tableau.", diff --git a/backend/i18n/source/backend_it.json b/backend/i18n/source/backend_it.json index 80c6c82cc..da5214956 100644 --- a/backend/i18n/source/backend_it.json +++ b/backend/i18n/source/backend_it.json @@ -231,6 +231,7 @@ "history.schemas.updated": "ha aggiornato lo schema {[Name]}.", "history.statusChanged": "ha cambiato lo stato del contenuto {[Schema]} in {[Status]}.", "login.githubPrivateEmail": "Il tuo indirizzo email è impostato su privato in Github. Impostalo come pubblico per poter utilizzare il login Github.", + "rules.validation.schemaNotFound": "Lo schema {id} non esiste.", "schemas.dateTimeCalculatedDefaultAndDefaultError": "Il valore predefinito calcolato e il valore predefinito non possono essere utilizzati insieme.", "schemas.duplicateFieldName": "Il campo '{field}' è stato aggiunto due volte.", "schemas.fieldCannotBeUIField": "Il campo non può essere un campo UI.", @@ -240,7 +241,6 @@ "schemas.fieldsNotCovered": "Non tutti i campi hanno degli ID associati.", "schemas.nameAlreadyExists": "Esiste già uno schema con lo stesso nome.", "schemas.noPermission": "Non hai i permessi per questo schema.", - "schemas.notFoundId": "Lo schema {id} non esiste.", "schemas.number.inlineEditorError": "Non è consentita per l'editor di tipo radio la modifica in linea.", "schemas.onlyArraysHaveNested": "Solo i campi di tipo array possono avere campi annidati.", "schemas.onylArraysInRoot": "Non è possibile annidare un campo di tipo array.", diff --git a/backend/i18n/source/backend_nl.json b/backend/i18n/source/backend_nl.json index 2ce3dbb7b..e308af7f5 100644 --- a/backend/i18n/source/backend_nl.json +++ b/backend/i18n/source/backend_nl.json @@ -242,6 +242,7 @@ "history.schemas.updated": "bijgewerkt schema {[Name]}.", "history.statusChanged": "veranderde status van {[Schema]} inhoud in {[Status]}.", "login.githubPrivateEmail": "Jouw e-mailadres is ingesteld op privé in Github. Stel het in op openbaar om Github-login te gebruiken.", + "rules.validation.schemaNotFound": "Schema {id} bestaat niet.", "schemas.dateTimeCalculatedDefaultAndDefaultError": "Berekende standaardwaarde en standaardwaarde kunnen niet samen worden gebruikt.", "schemas.duplicateFieldName": "Veld '{field}' is twee keer toegevoegd.", "schemas.fieldCannotBeUIField": "Veld mag geen UI-veld zijn.", @@ -251,7 +252,6 @@ "schemas.fieldsNotCovered": "Veld-id's dekken niet alle velden.", "schemas.nameAlreadyExists": "Er bestaat al een schema met dezelfde naam.", "schemas.noPermission": "Je hebt geen toestemming voor dit schema.", - "schemas.notFoundId": "Schema {id} bestaat niet.", "schemas.number.inlineEditorError": "Inline bewerken is niet toegestaan ​​voor Radio-editor.", "schemas.onlyArraysHaveNested": "Alleen matrixvelden kunnen geneste velden bevatten.", "schemas.onylArraysInRoot": "Genest veld mag geen matrixvelden zijn.", diff --git a/backend/i18n/source/backend_pt.json b/backend/i18n/source/backend_pt.json index f43280da3..6687984df 100644 --- a/backend/i18n/source/backend_pt.json +++ b/backend/i18n/source/backend_pt.json @@ -260,6 +260,7 @@ "history.teams.planReset": "plano redifinido.", "history.teams.updated": "actualizadas configurações gerais e renomeado para {[Name]}.", "login.githubPrivateEmail": "O seu Email é privado no Github. Altere para publico no Github e tente novamente.", + "rules.validation.schemaNotFound": "Esquema {id} não existe.", "schemas.dateTimeCalculatedDefaultAndDefaultError": "Valor por defeito calculado e valor por defeito não podem ser usado em conjunto.", "schemas.duplicateFieldName": "Campo '{field}' foi adicionado em duplicado.", "schemas.fieldCannotBeUIField": "Campo não pode ser um campo UI.", @@ -269,7 +270,6 @@ "schemas.fieldsNotCovered": "Ids do campo não cobrem todos os campos.", "schemas.nameAlreadyExists": "Um esquema com o mesmo nome já existe.", "schemas.noPermission": "Não tem permissões para este esquema.", - "schemas.notFoundId": "Esquema {id} não existe.", "schemas.number.inlineEditorError": "Não é permitido alteração em linha no Radio editor.", "schemas.onlyArraysHaveNested": "So campos lista podem ter campos aninhados.", "schemas.onylArraysInRoot": "Campo aninhado não pode ser lista de campos.", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index adc18f4ab..423e6cb84 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -336,6 +336,7 @@ "common.prevPage": "Previous Page", "common.product": "Squidex Headless CMS", "common.project": "Project", + "common.properties": "Properties", "common.queryOperators.contains": "contains", "common.queryOperators.empty": "is empty", "common.queryOperators.endsWith": "ends with", @@ -714,6 +715,15 @@ "rules.conditions.commentKeyword": "Only for text keywords", "rules.conditions.commentUser": "Specific users", "rules.conditions.contentValue": "Content has value", + "rules.conditions.cronExpression": "Cron Expression", + "rules.conditions.cronExpressionEvery4Hours": "Every 4 hours", + "rules.conditions.cronExpressionEveryMonth": "First day every month, at 6 AM in the morning", + "rules.conditions.cronExpressionEveryMorning": "Every day at 6 AM in the morning", + "rules.conditions.cronExpressionHint": "A cron expression is a string consisting of six or seven subexpressions (fields) that describe individual details of the schedule. You cannot create expressions that trigger this flow more often than every 4 hours.", + "rules.conditions.cronExpressionsHint": "Typical cron expressions", + "rules.conditions.cronExpressionsTitle": "Cron Expressions", + "rules.conditions.cronTimezone": "Timezone", + "rules.conditions.cronTimezoneHint": "The timezone as IANA format to define how to calculate the next time the flow should be run. Use your local timezone if a job should run every morning before working hours.", "rules.conditions.event": "Specific events", "rules.conditions.images": "Images only", "rules.conditions.largeAssets": "Large assets", @@ -761,6 +771,7 @@ "rules.ruleEvents.loadFailed": "Failed to load events. Please reload.", "rules.ruleEvents.nextAttemptLabel": "Next", "rules.ruleEvents.reloaded": "RuleEvents reloaded.", + "rules.ruleEvents.validationFailed": "Failed to validate rule.", "rules.ruleSimulator.listPageTitle": "Simulator", "rules.ruleSyntax.if": "when", "rules.ruleSyntax.then": "then", @@ -783,7 +794,7 @@ "rules.simulation.errorTooOld": "STOP: Event is too old.", "rules.simulation.errorWrongEvent": "STOP: Event does not match to the trigger.", "rules.simulation.errorWrongEventForTrigger": "STOP: Event does not match to the trigger.", - "rules.simulation.eventConditionEvaluated": "Enriched event is evaluated, whether it matchs to the conditions and javascript expressions in the trigger.", + "rules.simulation.eventConditionEvaluated": "Enriched is tested against the conditions and javascript expressions in the trigger.", "rules.simulation.eventEnriched": "Event is enriched with additional data", "rules.simulation.eventQueried": "Event is queried from the database", "rules.simulation.eventTriggerChecked": "Event is tested to see if it matchs to the trigger and the basic conditions.", diff --git a/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs b/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs index e003a65ef..1a632b599 100644 --- a/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs @@ -37,11 +37,12 @@ public abstract class AppDbContext(DbContextOptions options, IJsonSerializer jso builder.UseAssetKeyValueStore(); builder.UseAssets(jsonSerializer, jsonColumnType); builder.UseCache(); - builder.UseCounters(jsonSerializer, jsonColumnType); builder.UseChatStore(); builder.UseContent(jsonSerializer, jsonColumnType, string.Empty); builder.UseContentReferences(string.Empty); builder.UseContentTables(); + builder.UseCounters(jsonSerializer, jsonColumnType); + builder.UseCronJobs(); builder.UseEvents(jsonSerializer, jsonColumnType); builder.UseEventStore(); builder.UseFlows(); diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250504173108_AddFlows.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250504173108_AddFlows.cs index aba9e8eca..53aec9f39 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250504173108_AddFlows.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250504173108_AddFlows.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.Designer.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.Designer.cs new file mode 100644 index 000000000..26adb3e8c --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.Designer.cs @@ -0,0 +1,1586 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Squidex.Providers.MySql.App; + +#nullable disable + +namespace Squidex.Providers.MySql.App.Migrations +{ + [DbContext(typeof(MySqlAppDbContext))] + [Migration("20250513192106_AddCronJobs")] + partial class AddCronJobs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("varchar(255)"); + + b.Property("ApplicationId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("Scopes") + .HasColumnType("longtext"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("varchar(255)"); + + b.Property("ApplicationId") + .HasColumnType("varchar(255)"); + + b.Property("AuthorizationId") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Payload") + .HasColumnType("longtext"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("RedemptionDate") + .HasColumnType("datetime(6)"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Squidex.AI.Mongo.EFChatEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("LastUpdated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("LastUpdated"); + + b.ToTable("Chats", (string)null); + }); + + modelBuilder.Entity("Squidex.Assets.EntityFramework.EFAssetKeyValueEntity", b => + { + b.Property("Key") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Key"); + + b.HasIndex("Expires"); + + b.ToTable("AssetKeyValueStore_TusMetadata", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Apps.EFAppEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("IndexedCreated") + .HasColumnType("datetime(6)") + .HasColumnName("Created"); + + b.Property("IndexedDeleted") + .HasColumnType("tinyint(1)") + .HasColumnName("Deleted"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("Name"); + + b.Property("IndexedTeamId") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("TeamId"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_App", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("FileHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileVersion") + .HasColumnType("bigint"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("IsProtected") + .HasColumnType("tinyint(1)"); + + b.Property("LastModified") + .HasColumnType("datetime(6)"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("json"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Tags") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("TotalSize") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("Assets"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetFolderEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("FolderName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastModified") + .HasColumnType("datetime(6)"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("AssetFolders"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("json"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastModified") + .HasColumnType("datetime(6)"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("NewData") + .HasColumnType("json"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ScheduleJob") + .HasColumnType("json"); + + b.Property("ScheduledAt") + .HasColumnType("datetime(6)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("TranslationStatus") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("json"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastModified") + .HasColumnType("datetime(6)"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("NewData") + .HasColumnType("json"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ScheduleJob") + .HasColumnType("json"); + + b.Property("ScheduledAt") + .HasColumnType("datetime(6)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("TranslationStatus") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentTableEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("AppId", "SchemaId") + .IsUnique(); + + b.ToTable("ContentTables", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferenceCompleteEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("FromSchema") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasDefaultValue("00000000-0000-0000-0000-000000000000"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferencePublishedEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("FromSchema") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasDefaultValue("00000000-0000-0000-0000-000000000000"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexGeoEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("GeoField") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("GeoObject") + .IsRequired() + .HasColumnType("geometry"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ServeAll") + .HasColumnType("tinyint(1)"); + + b.Property("ServePublished") + .HasColumnType("tinyint(1)"); + + b.Property("Stage") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.ToTable("Geos", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexTextEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ServeAll") + .HasColumnType("tinyint(1)"); + + b.Property("ServePublished") + .HasColumnType("tinyint(1)"); + + b.Property("Stage") + .HasColumnType("tinyint unsigned"); + + b.Property("Texts") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Texts", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.State.TextContentState", b => + { + b.Property("UniqueContentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("UniqueContentId"); + + b.ToTable("TextState", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.History.HistoryEvent", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Parameters") + .IsRequired() + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("HistoryEvent"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Rules.EFRuleEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("tinyint(1)") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Id"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Rule", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Schemas.EFSchemaEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("tinyint(1)") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Id"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("Name"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Schema", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Teams.EFTeamEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("IndexedAuthDomain") + .HasColumnType("longtext") + .HasColumnName("AuthDomain"); + + b.Property("IndexedDeleted") + .HasColumnType("tinyint(1)") + .HasColumnName("Deleted"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Team", (string)null); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFEventCommit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EventStream") + .IsRequired() + .HasMaxLength(750) + .HasColumnType("varchar(750)"); + + b.Property("EventStreamOffset") + .HasColumnType("bigint"); + + b.Property("Events") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EventsCount") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("EventStream", "EventStreamOffset") + .IsUnique(); + + b.HasIndex("EventStream", "Position"); + + b.HasIndex("EventStream", "Timestamp"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFPosition", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("EventPosition"); + }); + + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFCronJobEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DueTime") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DueTime"); + + b.ToTable("CronJobs", (string)null); + }); + + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFFlowStateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("DefinitionId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("DueTime") + .HasColumnType("datetime(6)"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("SchedulePartition") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("DueTime", "SchedulePartition"); + + b.ToTable("Flows", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Caching.EFCacheEntity", b => + { + b.Property("Key") + .HasColumnType("varchar(255)"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Key"); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Log.EFRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Properties") + .IsRequired() + .HasColumnType("json"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Key"); + + b.ToTable("Requests", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Migrations.EFMigrationEntity", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("IsLocked") + .HasColumnType("tinyint(1)"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Migrations", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UISettings", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_TagHistory", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageNotifications", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Counters", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_JobsState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageTracker", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_Tags", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Keys", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Xml", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_EventConsumerState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Document") + .HasColumnType("json"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Names", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.UsageTracking.EFUsageCounterEntity", b => + { + b.Property("Key") + .HasColumnType("varchar(255)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("Category") + .HasColumnType("varchar(255)"); + + b.Property("CounterKey") + .HasColumnType("varchar(255)"); + + b.Property("CounterValue") + .HasColumnType("double"); + + b.HasKey("Key", "Date", "Category", "CounterKey"); + + b.ToTable("Counter", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessage", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ChannelName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("MessageData") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("MessageHeaders") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("QueueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("TimeHandled") + .HasColumnType("datetime(6)"); + + b.Property("TimeToLive") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ChannelName", "TimeHandled"); + + b.ToTable("Messages", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessagingDataEntity", b => + { + b.Property("Group") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Key") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Expiration") + .HasColumnType("datetime(6)"); + + b.Property("ValueData") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("ValueFormat") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ValueType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.HasKey("Group", "Key"); + + b.HasIndex("Expiration"); + + b.ToTable("MessagingData", (string)null); + }); + + modelBuilder.Entity("YDotNet.Server.EntityFramework.YDotNetDocument", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("Expiration") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("YDotNetDocument", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.cs new file mode 100644 index 000000000..254ef4351 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Squidex.Providers.MySql.App.Migrations +{ + /// + public partial class AddCronJobs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CronJobs", + columns: table => new + { + Id = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + DueTime = table.Column(type: "datetime(6)", nullable: false), + Data = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_CronJobs", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_CronJobs_DueTime", + table: "CronJobs", + column: "DueTime"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CronJobs"); + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/MySqlDbContextModelSnapshot.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/MySqlDbContextModelSnapshot.cs index bbf211c5e..1e061c61d 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/MySqlDbContextModelSnapshot.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/MySqlDbContextModelSnapshot.cs @@ -1097,6 +1097,26 @@ namespace Squidex.Providers.MySql.Migrations b.ToTable("EventPosition"); }); + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFCronJobEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DueTime") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DueTime"); + + b.ToTable("CronJobs", (string)null); + }); + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFFlowStateEntity", b => { b.Property("Id") diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250504173116_AddFlows.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250504173116_AddFlows.cs index 05656af80..587b05c31 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250504173116_AddFlows.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250504173116_AddFlows.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.Designer.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.Designer.cs new file mode 100644 index 000000000..2ce7bf1cc --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.Designer.cs @@ -0,0 +1,1587 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Squidex.Providers.Postgres.App; + +#nullable disable + +namespace Squidex.Providers.Postgres.App.Migrations +{ + [DbContext(typeof(PostgresAppDbContext))] + [Migration("20250513192113_AddCronJobs")] + partial class AddCronJobs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Squidex.AI.Mongo.EFChatEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("LastUpdated"); + + b.ToTable("Chats", (string)null); + }); + + modelBuilder.Entity("Squidex.Assets.EntityFramework.EFAssetKeyValueEntity", b => + { + b.Property("Key") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Key"); + + b.HasIndex("Expires"); + + b.ToTable("AssetKeyValueStore_TusMetadata", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Apps.EFAppEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("IndexedCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("Created"); + + b.Property("IndexedDeleted") + .HasColumnType("boolean") + .HasColumnName("Deleted"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("Name"); + + b.Property("IndexedTeamId") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("TeamId"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("text") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_App", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("FileHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileVersion") + .HasColumnType("bigint"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsProtected") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tags") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TotalSize") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("Assets"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetFolderEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("FolderName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("AssetFolders"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NewData") + .HasColumnType("jsonb"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ScheduleJob") + .HasColumnType("jsonb"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TranslationStatus") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NewData") + .HasColumnType("jsonb"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ScheduleJob") + .HasColumnType("jsonb"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TranslationStatus") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentTableEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("AppId", "SchemaId") + .IsUnique(); + + b.ToTable("ContentTables", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferenceCompleteEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FromSchema") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasDefaultValue("00000000-0000-0000-0000-000000000000"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferencePublishedEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FromSchema") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasDefaultValue("00000000-0000-0000-0000-000000000000"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexGeoEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("GeoField") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("GeoObject") + .IsRequired() + .HasColumnType("geometry"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ServeAll") + .HasColumnType("boolean"); + + b.Property("ServePublished") + .HasColumnType("boolean"); + + b.Property("Stage") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Geos", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexTextEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ServeAll") + .HasColumnType("boolean"); + + b.Property("ServePublished") + .HasColumnType("boolean"); + + b.Property("Stage") + .HasColumnType("smallint"); + + b.Property("Texts") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Texts", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.State.TextContentState", b => + { + b.Property("UniqueContentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("UniqueContentId"); + + b.ToTable("TextState", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.History.HistoryEvent", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Parameters") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("HistoryEvent"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Rules.EFRuleEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("boolean") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("Id"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Rule", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Schemas.EFSchemaEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("boolean") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("Id"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("Name"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Schema", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Teams.EFTeamEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("IndexedAuthDomain") + .HasColumnType("text") + .HasColumnName("AuthDomain"); + + b.Property("IndexedDeleted") + .HasColumnType("boolean") + .HasColumnName("Deleted"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("text") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Team", (string)null); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFEventCommit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EventStream") + .IsRequired() + .HasMaxLength(750) + .HasColumnType("character varying(750)"); + + b.Property("EventStreamOffset") + .HasColumnType("bigint"); + + b.Property("Events") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("EventsCount") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("EventStream", "EventStreamOffset") + .IsUnique(); + + b.HasIndex("EventStream", "Position"); + + b.HasIndex("EventStream", "Timestamp"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFPosition", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("EventPosition"); + }); + + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFCronJobEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("DueTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DueTime"); + + b.ToTable("CronJobs", (string)null); + }); + + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFFlowStateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("DefinitionId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("DueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SchedulePartition") + .HasColumnType("integer"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DueTime", "SchedulePartition"); + + b.ToTable("Flows", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Caching.EFCacheEntity", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Key"); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Log.EFRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Properties") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key"); + + b.ToTable("Requests", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Migrations.EFMigrationEntity", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Migrations", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UISettings", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_TagHistory", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageNotifications", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Counters", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_JobsState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageTracker", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_Tags", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Keys", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Xml", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_EventConsumerState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Document") + .HasColumnType("jsonb"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Names", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.UsageTracking.EFUsageCounterEntity", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("CounterKey") + .HasColumnType("text"); + + b.Property("CounterValue") + .HasColumnType("double precision"); + + b.HasKey("Key", "Date", "Category", "CounterKey"); + + b.ToTable("Counter", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ChannelName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("MessageData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("MessageHeaders") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("QueueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TimeHandled") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeToLive") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ChannelName", "TimeHandled"); + + b.ToTable("Messages", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessagingDataEntity", b => + { + b.Property("Group") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Key") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ValueData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ValueFormat") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ValueType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Group", "Key"); + + b.HasIndex("Expiration"); + + b.ToTable("MessagingData", (string)null); + }); + + modelBuilder.Entity("YDotNet.Server.EntityFramework.YDotNetDocument", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("YDotNetDocument", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.cs new file mode 100644 index 000000000..d68daddd9 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Squidex.Providers.Postgres.App.Migrations +{ + /// + public partial class AddCronJobs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CronJobs", + columns: table => new + { + Id = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + DueTime = table.Column(type: "timestamp with time zone", nullable: false), + Data = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CronJobs", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_CronJobs_DueTime", + table: "CronJobs", + column: "DueTime"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CronJobs"); + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/PostgresDbContextModelSnapshot.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/PostgresDbContextModelSnapshot.cs index c1d423c50..6d3c6b162 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/PostgresDbContextModelSnapshot.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/PostgresDbContextModelSnapshot.cs @@ -1098,6 +1098,26 @@ namespace Squidex.Providers.Postgres.Migrations b.ToTable("EventPosition"); }); + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFCronJobEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("DueTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DueTime"); + + b.ToTable("CronJobs", (string)null); + }); + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFFlowStateEntity", b => { b.Property("Id") diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250504173123_AddFlows.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250504173123_AddFlows.cs index 99b2395dd..f31e33c53 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250504173123_AddFlows.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250504173123_AddFlows.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.Designer.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.Designer.cs new file mode 100644 index 000000000..b5873f7c7 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.Designer.cs @@ -0,0 +1,1589 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Squidex.Providers.SqlServer.App; + +#nullable disable + +namespace Squidex.Providers.SqlServer.App.Migrations +{ + [DbContext(typeof(SqlServerAppDbContext))] + [Migration("20250513192120_AddCronJobs")] + partial class AddCronJobs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Scopes") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("AuthorizationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedemptionDate") + .HasColumnType("datetime2"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasFilter("[ReferenceId] IS NOT NULL"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Squidex.AI.Mongo.EFChatEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("LastUpdated") + .HasColumnType("datetime2"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("LastUpdated"); + + b.ToTable("Chats", (string)null); + }); + + modelBuilder.Entity("Squidex.Assets.EntityFramework.EFAssetKeyValueEntity", b => + { + b.Property("Key") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Expires") + .HasColumnType("datetimeoffset"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Key"); + + b.HasIndex("Expires"); + + b.ToTable("AssetKeyValueStore_TusMetadata", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Apps.EFAppEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("IndexedCreated") + .HasColumnType("datetimeoffset") + .HasColumnName("Created"); + + b.Property("IndexedDeleted") + .HasColumnType("bit") + .HasColumnName("Deleted"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("Name"); + + b.Property("IndexedTeamId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("TeamId"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_App", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("FileHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileVersion") + .HasColumnType("bigint"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsProtected") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("TotalSize") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("Assets"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Assets.EFAssetFolderEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("FolderName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ParentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.HasIndex("IndexedAppId", "Id"); + + b.ToTable("AssetFolders"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("NewData") + .HasColumnType("nvarchar(max)"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ScheduleJob") + .HasColumnType("nvarchar(max)"); + + b.Property("ScheduledAt") + .HasColumnType("datetimeoffset"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TranslationStatus") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IndexedSchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("NewData") + .HasColumnType("nvarchar(max)"); + + b.Property("NewStatus") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ScheduleJob") + .HasColumnType("nvarchar(max)"); + + b.Property("ScheduledAt") + .HasColumnType("datetimeoffset"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TranslationStatus") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("ContentsPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentTableEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("AppId", "SchemaId") + .IsUnique(); + + b.ToTable("ContentTables", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferenceCompleteEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FromSchema") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasDefaultValue("00000000-0000-0000-0000-000000000000"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesAll", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferencePublishedEntity", b => + { + b.Property("AppId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FromKey") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ToId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FromSchema") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasDefaultValue("00000000-0000-0000-0000-000000000000"); + + b.HasKey("AppId", "FromKey", "ToId"); + + b.ToTable("ContentReferencesPublished", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexGeoEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("GeoField") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("GeoObject") + .IsRequired() + .HasColumnType("geography"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ServeAll") + .HasColumnType("bit"); + + b.Property("ServePublished") + .HasColumnType("bit"); + + b.Property("Stage") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.ToTable("Geos", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexTextEntity", b => + { + b.Property("Id") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ContentId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("SchemaId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ServeAll") + .HasColumnType("bit"); + + b.Property("ServePublished") + .HasColumnType("bit"); + + b.Property("Stage") + .HasColumnType("tinyint"); + + b.Property("Texts") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Texts", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.State.TextContentState", b => + { + b.Property("UniqueContentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("UniqueContentId"); + + b.ToTable("TextState", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.History.HistoryEvent", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Channel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Parameters") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("HistoryEvent"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Rules.EFRuleEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("bit") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("Id"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Rule", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Schemas.EFSchemaEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("IndexedAppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("AppId"); + + b.Property("IndexedDeleted") + .HasColumnType("bit") + .HasColumnName("Deleted"); + + b.Property("IndexedId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("Id"); + + b.Property("IndexedName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("Name"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Schema", (string)null); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Teams.EFTeamEntity", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("IndexedAuthDomain") + .HasColumnType("nvarchar(max)") + .HasColumnName("AuthDomain"); + + b.Property("IndexedDeleted") + .HasColumnType("bit") + .HasColumnName("Deleted"); + + b.Property("IndexedUserIds") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("UserIds"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Team", (string)null); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFEventCommit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EventStream") + .IsRequired() + .HasMaxLength(750) + .HasColumnType("nvarchar(750)"); + + b.Property("EventStreamOffset") + .HasColumnType("bigint"); + + b.Property("Events") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventsCount") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.Property("Timestamp") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("EventStream", "EventStreamOffset") + .IsUnique(); + + b.HasIndex("EventStream", "Position"); + + b.HasIndex("EventStream", "Timestamp"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("Squidex.Events.EntityFramework.EFPosition", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("EventPosition"); + }); + + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFCronJobEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DueTime") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("DueTime"); + + b.ToTable("CronJobs", (string)null); + }); + + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFFlowStateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("DefinitionId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("DueTime") + .HasColumnType("datetimeoffset"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("SchedulePartition") + .HasColumnType("int"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DueTime", "SchedulePartition"); + + b.ToTable("Flows", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Caching.EFCacheEntity", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Expires") + .HasColumnType("datetime2"); + + b.Property("Value") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.HasKey("Key"); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Log.EFRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Properties") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("Key"); + + b.ToTable("Requests", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.Migrations.EFMigrationEntity", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("IsLocked") + .HasColumnType("bit"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Migrations", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UISettings", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_TagHistory", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageNotifications", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Counters", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_JobsState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_UsageTracker", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Index_Tags", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Keys", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Identity_Xml", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_EventConsumerState", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.States.EFState", b => + { + b.Property("DocumentId") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Document") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("DocumentId"); + + b.ToTable("States_Names", (string)null); + }); + + modelBuilder.Entity("Squidex.Infrastructure.UsageTracking.EFUsageCounterEntity", b => + { + b.Property("Key") + .HasColumnType("nvarchar(450)"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("Category") + .HasColumnType("nvarchar(450)"); + + b.Property("CounterKey") + .HasColumnType("nvarchar(450)"); + + b.Property("CounterValue") + .HasColumnType("float"); + + b.HasKey("Key", "Date", "Category", "CounterKey"); + + b.ToTable("Counter", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessage", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ChannelName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("MessageData") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("MessageHeaders") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("QueueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("TimeHandled") + .HasColumnType("datetime2"); + + b.Property("TimeToLive") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ChannelName", "TimeHandled"); + + b.ToTable("Messages", (string)null); + }); + + modelBuilder.Entity("Squidex.Messaging.EntityFramework.EFMessagingDataEntity", b => + { + b.Property("Group") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Key") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Expiration") + .HasColumnType("datetime2"); + + b.Property("ValueData") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("ValueFormat") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ValueType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.HasKey("Group", "Key"); + + b.HasIndex("Expiration"); + + b.ToTable("MessagingData", (string)null); + }); + + modelBuilder.Entity("YDotNet.Server.EntityFramework.YDotNetDocument", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("Expiration") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("YDotNetDocument", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.cs new file mode 100644 index 000000000..781bf6077 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Squidex.Providers.SqlServer.App.Migrations +{ + /// + public partial class AddCronJobs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CronJobs", + columns: table => new + { + Id = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + DueTime = table.Column(type: "datetimeoffset", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CronJobs", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_CronJobs_DueTime", + table: "CronJobs", + column: "DueTime"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CronJobs"); + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/SqlServerDbContextModelSnapshot.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/SqlServerDbContextModelSnapshot.cs index b9a3700fe..972a4b948 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/SqlServerDbContextModelSnapshot.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/SqlServerDbContextModelSnapshot.cs @@ -1100,6 +1100,26 @@ namespace Squidex.Providers.SqlServer.Migrations b.ToTable("EventPosition"); }); + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFCronJobEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DueTime") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("DueTime"); + + b.ToTable("CronJobs", (string)null); + }); + modelBuilder.Entity("Squidex.Flows.EntityFramework.EFFlowStateEntity", b => { b.Property("Id") diff --git a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs index 7fd129f43..6ed6c7ecc 100644 --- a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs +++ b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs @@ -10,7 +10,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Squidex.AI; using Squidex.Assets.TusAdapter; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Assets; @@ -209,6 +208,9 @@ public static class ServiceExtensions services.AddFlowsCore() .AddEntityFrameworkStore(); + services.AddCronJobsCore() + .AddEntityFrameworkStore(); + services.AddEntityFrameworkAssetKeyValueStore(); } diff --git a/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj b/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj index fe4649109..dd7cfec2d 100644 --- a/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj +++ b/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj @@ -36,17 +36,17 @@ - - - + + + - - - - + + + + diff --git a/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs b/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs index 1723cfb91..544a43e0a 100644 --- a/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs +++ b/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs @@ -16,7 +16,6 @@ using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Driver.Core.Extensions.DiagnosticSources; using MongoDB.Driver.GridFS; -using Squidex.AI; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Contents; @@ -186,7 +185,10 @@ public static class ServiceExtensions .As>().As(); services.AddFlowsCore() - .AddMongoFlowStore(); + .AddMongoStore(); + + services.AddCronJobsCore() + .AddMongoStore(); services.AddSingletonAs(c => { diff --git a/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj b/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj index bb323fc7b..10ee3e6b9 100644 --- a/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj +++ b/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj @@ -25,12 +25,12 @@ - - - - - - + + + + + + diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCronJobEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCronJobEvent.cs new file mode 100644 index 000000000..2b2786190 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCronJobEvent.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public sealed class EnrichedCronJobEvent : EnrichedUserEventBase +{ + public JsonValue Value { get; set; } + + public override long Partition + { + get => 0; + } +} 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 b1245db11..707261eef 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs @@ -17,6 +17,8 @@ public interface IRuleTriggerVisitor T Visit(CommentTrigger trigger, TArgs args); + T Visit(CronJobTrigger trigger, TArgs args); + T Visit(ManualTrigger trigger, TArgs args); T Visit(SchemaChangedTrigger trigger, TArgs args); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CronJobTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CronJobTrigger.cs new file mode 100644 index 000000000..92fb17887 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CronJobTrigger.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Core.Rules.Triggers; + +[TypeName(nameof(CronJobTrigger))] +public sealed record CronJobTrigger : RuleTrigger +{ + public string CronExpression { get; init; } + + public string? CronTimezone { get; init; } + + public JsonValue Value { get; init; } + + public override T Accept(IRuleTriggerVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index 5afcc1ccd..0d380107e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -20,7 +20,7 @@ - + diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/CronJobContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/CronJobContext.cs new file mode 100644 index 000000000..aed2cead4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/CronJobContext.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed record CronJobContext(NamedId AppId, DomainId RuleId); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index b1179d54a..baf6f0447 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -29,8 +29,8 @@ - - + + diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/CronJobTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/CronJobTriggerHandler.cs new file mode 100644 index 000000000..a88df52fb --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/CronJobTriggerHandler.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Runtime.CompilerServices; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Rules; + +public sealed class CronJobTriggerHandler : IRuleTriggerHandler +{ + public Type TriggerType => typeof(CronJobTrigger); + + public bool Handles(AppEvent appEvent) + { + return appEvent is RuleCronJobTriggered; + } + + public async IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RulesContext context, + [EnumeratorCancellation] CancellationToken ct) + { + var result = new EnrichedCronJobEvent(); + + // Use the concrete event to map properties that are not part of app event. + SimpleMapper.Map((RuleCronJobTriggered)@event.Payload, result); + + await Task.Yield(); + yield return result; + } + + public string? GetName(AppEvent @event) + { + return "CronJob"; + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/CronJobUpdater.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/CronJobUpdater.cs new file mode 100644 index 000000000..74e627fcb --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/CronJobUpdater.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Events; +using Squidex.Flows.CronJobs; +using Squidex.Hosting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Rules; + +public sealed class CronJobUpdater( + IAppProvider appProvider, + ICronJobManager cronJobs, + IRuleEnqueuer ruleEnqueuer) + : IEventConsumer, IInitializable +{ + public StreamFilter EventsFilter => StreamFilter.Prefix("rule-"); + + public Task InitializeAsync( + CancellationToken ct) + { + cronJobs.Subscribe(HandleCronJobAsync); + return Task.CompletedTask; + } + + public async Task HandleCronJobAsync(CronJob job, + CancellationToken ct) + { + var (appId, ruleId) = job.Context; + + var rule = await appProvider.GetRuleAsync(appId.Id, ruleId, ct); + + // The rule might have been updated or deleted in the meantime, but we are running asynchronously. + if (rule == null || rule.Trigger is not CronJobTrigger cronJob) + { + return; + } + + // The rule enqueue needs an event. + var @event = new RuleCronJobTriggered { AppId = appId, RuleId = ruleId, Value = cronJob.Value }; + + await ruleEnqueuer.EnqueueAsync(rule, Envelope.Create(@event), ct); + } + + public async Task On(Envelope @event) + { + if (@event.Payload is RuleCreated created) + { + if (created.Trigger is CronJobTrigger cronJob) + { + await AddCronJobAsync(created.AppId, created.RuleId, cronJob, default); + } + } + else if (@event.Payload is RuleUpdated updated && updated.Trigger != null) + { + if (updated.Trigger is CronJobTrigger cronJob) + { + await AddCronJobAsync(updated.AppId, updated.RuleId, cronJob, default); + } + else + { + await cronJobs.RemoveAsync(updated.RuleId.ToString()); + } + } + else if (@event.Payload is RuleDeleted deleted) + { + await cronJobs.RemoveAsync(deleted.RuleId.ToString()); + } + } + + private async Task AddCronJobAsync(NamedId appId, DomainId id, CronJobTrigger trigger, + CancellationToken ct) + { + await cronJobs.AddAsync(new CronJob + { + Id = id.ToString(), + CronExpression = trigger.CronExpression, + CronTimezone = trigger.CronTimezone, + Context = new CronJobContext(appId, id), + }, ct); + } +} 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 7a0e50668..ffa671f90 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 @@ -43,8 +43,7 @@ public static class GuardRule }); } - public static Task CanUpdate(UpdateRule command, Rule rule, IRuleValidator validator, - CancellationToken ct) + public static Task CanUpdate(UpdateRule command, IRuleValidator validator, CancellationToken ct) { Guard.NotNull(command); 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 b833c68ba..c453e2fd7 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 @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Org.BouncyCastle.Pkcs; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Rules; 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 5f526e1f2..6e4081d44 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs @@ -72,7 +72,7 @@ public partial class RuleDomainObject( case UpdateRule updateRule: return ApplyReturnAsync(updateRule, async (c, ct) => { - await GuardRule.CanUpdate(c, Snapshot, Validator(), ct); + await GuardRule.CanUpdate(c, Validator(), ct); Update(c); @@ -122,7 +122,7 @@ public partial class RuleDomainObject( SimpleMapper.Map(command, @event); SimpleMapper.Map(Snapshot, @event); - await Enqueuer().EnqueueAsync(Snapshot.Id, Snapshot, Envelope.Create(@event)); + await Enqueuer().EnqueueAsync(Snapshot, Envelope.Create(@event)); } private void Create(CreateRule command) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs index b387eb0a4..3865fe2d7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs @@ -13,5 +13,6 @@ namespace Squidex.Domain.Apps.Entities.Rules; public interface IRuleEnqueuer { - Task EnqueueAsync(DomainId ruleId, Rule rule, Envelope @event); + Task EnqueueAsync(Rule rule, Envelope @event, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index 681258328..457188f3a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -43,10 +43,11 @@ public sealed class RuleEnqueuer( get => GetType().Name; } - public async Task EnqueueAsync(DomainId ruleId, Rule rule, Envelope @event) + public async Task EnqueueAsync(Rule rule, Envelope @event, + CancellationToken ct = default) { Guard.NotNull(rule); - Guard.NotNull(@event, nameof(@event)); + Guard.NotNull(@event); if (@event.Payload is not AppEvent appEvent) { @@ -58,16 +59,14 @@ public sealed class RuleEnqueuer( AppId = appEvent.AppId, IncludeSkipped = false, IncludeStale = false, - Rules = new Dictionary + Rules = rule != null ? new Dictionary { - [ruleId] = rule, - }.ToReadonlyDictionary(), + [rule.Id] = rule, + }.ToReadonlyDictionary() : [], }; - // Write in batches of 100 items for better performance. Dispose completes the last write. await using var batch = new RuleQueueWriter(flowManager, ruleUsageTracker, log); - - await foreach (var result in ruleService.CreateJobsAsync(@event, context)) + await foreach (var result in ruleService.CreateJobsAsync(@event, context, ct)) { await batch.WriteAsync(appEvent.AppId.Id, result); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobUpdate.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobUpdate.cs deleted file mode 100644 index c841f6d37..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobUpdate.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; - -namespace Squidex.Domain.Apps.Entities.Rules; - -public sealed class RuleJobUpdate -{ - public string? ExecutionDump { get; set; } - - public RuleResult ExecutionResult { get; set; } - - public RuleJobResult JobResult { get; set; } - - public TimeSpan Elapsed { get; set; } - - public Instant Finished { get; set; } - - public Instant? JobNext { get; set; } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleValidator.cs index 589e4428d..6ba42550f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleValidator.cs @@ -9,6 +9,7 @@ using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Flows; +using Squidex.Flows.CronJobs; using Squidex.Flows.Internal; using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; @@ -16,7 +17,11 @@ using Squidex.Infrastructure.Validation; namespace Squidex.Domain.Apps.Entities.Rules; -public sealed class RuleValidator(IFlowManager flowManager, IAppProvider appProvider) : IRuleValidator +public sealed class RuleValidator( + IFlowManager flowManager, + ICronJobManager cronJobs, + IAppProvider appProvider) + : IRuleValidator { public async Task ValidateTriggerAsync(RuleTrigger trigger, DomainId appId, AddValidation addError, CancellationToken ct = default) @@ -24,7 +29,7 @@ public sealed class RuleValidator(IFlowManager flowManager, IA Guard.NotNull(trigger); Guard.NotNull(addError); - var context = new TriggerValidationContext(appId, addError, appProvider, ct); + var context = new TriggerValidationContext(appId, addError, appProvider, cronJobs, ct); await trigger.Accept(RuleTriggerValidator.Instance, context); } @@ -106,6 +111,21 @@ public sealed class RuleValidator(IFlowManager flowManager, IA return default; } + public ValueTask Visit(CronJobTrigger trigger, TriggerValidationContext args) + { + if (!args.CronJobs.IsValidCronExpression(trigger.CronExpression)) + { + args.AddError(T.Get("rules.validation.invalidCronExpression"), nameof(trigger.CronExpression)); + } + + if (!args.CronJobs.IsValidTimezone(trigger.CronTimezone)) + { + args.AddError(T.Get("rules.validation.invalidCronTimezone"), nameof(trigger.CronTimezone)); + } + + return default; + } + public async ValueTask Visit(ContentChangedTriggerV2 trigger, TriggerValidationContext args) { if (trigger.Schemas == null) @@ -121,7 +141,7 @@ public sealed class RuleValidator(IFlowManager flowManager, IA } else if (await args.AppProvider.GetSchemaAsync(args.AppId, schema.SchemaId, false, args.CancellationToken) == null) { - args.AddError(T.Get("schemas.notFoundId", new { id = schema.SchemaId }), nameof(trigger.Schemas)); + args.AddError(T.Get("rules.validation.schemaNotFound", new { id = schema.SchemaId }), nameof(trigger.Schemas)); } } @@ -135,6 +155,7 @@ public sealed class RuleValidator(IFlowManager flowManager, IA DomainId AppId, AddValidation AddError, IAppProvider AppProvider, + ICronJobManager CronJobs, CancellationToken CancellationToken); #pragma warning restore RECS0082 // Parameter has the same name as a member and hides it #pragma warning restore SA1313 // Parameter names should begin with lower-case letter 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 2172a9ba5..af0ed131a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Runtime.CompilerServices; using NodaTime; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.HandleRules; @@ -12,8 +13,10 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Entities.Jobs; using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Rules; using Squidex.Events; using Squidex.Flows; +using Squidex.Flows.Internal; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.EventSourcing; @@ -29,60 +32,67 @@ public sealed class DefaultRuleRunnerService( : IRuleRunnerService { private const int MaxSimulatedEvents = 100; + private static readonly RefToken SimulatorUser = RefToken.Client("Simulator"); - public Task> SimulateAsync(Rule rule, + public IClock Clock { get; set; } = SystemClock.Instance; + + public Task> SimulateAsync(NamedId appId, RuleTrigger trigger, FlowDefinition flow, CancellationToken ct = default) { - return SimulateAsync(rule.AppId, rule.Id, rule, ct); + var now = SystemClock.Instance.GetCurrentInstant(); + + var rule = new Rule + { + AppId = appId, + Created = now, + CreatedBy = SimulatorUser, + Flow = flow, + IsEnabled = true, + LastModified = now, + LastModifiedBy = SimulatorUser, + Trigger = trigger, + }; + + return SimulateAsync(rule, ct); } - public async Task> SimulateAsync(NamedId appId, DomainId ruleId, Rule rule, + public async Task> SimulateAsync(Rule rule, CancellationToken ct = default) { Guard.NotNull(rule); var context = new RulesContext { - AppId = appId, + AppId = rule.AppId, IncludeSkipped = true, IncludeStale = true, Rules = new Dictionary { - [ruleId] = rule, + [rule.Id] = rule, }.ToReadonlyDictionary(), }; var simulatedEvents = new List(MaxSimulatedEvents); - var streamStart = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromDays(7)).ToDateTimeUtc(); - var streamFilter = StreamFilter.Prefix($"%-{appId.Id}"); - - await foreach (var storedEvent in eventStore.QueryAllReverseAsync(streamFilter, streamStart, MaxSimulatedEvents, ct)) + await foreach (var appEvent in QueryEventsAsync(rule, ct)) { - var @event = eventFormatter.ParseIfKnown(storedEvent); - - if (@event?.Payload is not AppEvent appEvent) - { - continue; - } - // Also create jobs for rules with failing conditions because we want to show them in the table. - await foreach (var job in ruleService.CreateJobsAsync(@event, context, ct).Take(MaxSimulatedEvents).WithCancellation(ct)) + await foreach (var job in ruleService.CreateJobsAsync(appEvent, context, ct).Take(MaxSimulatedEvents).WithCancellation(ct)) { var state = job.Job != null ? await flowManager.SimulateAsync(job.Job.Value, ct) : null; - var eventId = @event.Headers.EventId(); + var eventId = appEvent.Headers.EventId(); simulatedEvents.Add(new SimulatedRuleEvent { EnrichedEvent = job.EnrichedEvent, Error = job.EnrichmentError?.Message, - Event = @event.Payload, + Event = appEvent.Payload, EventId = eventId, - EventName = ruleService.GetName(appEvent), + EventName = ruleService.GetName(appEvent.Payload), State = state, SkipReason = job.SkipReason, UniqueId = $"{eventId}_{job.Offset}", @@ -93,6 +103,52 @@ public sealed class DefaultRuleRunnerService( return simulatedEvents; } + private async IAsyncEnumerable> QueryEventsAsync(Rule rule, + [EnumeratorCancellation] CancellationToken ct) + { + var appId = rule.AppId; + + if (rule.Trigger is ManualTrigger) + { + yield return Envelope.Create( + new RuleManuallyTriggered + { + Actor = SimulatorUser, + AppId = appId, + RuleId = rule.Id, + }); + + yield break; + } + + if (rule.Trigger is CronJobTrigger cronJob) + { + yield return Envelope.Create( + new RuleCronJobTriggered + { + Actor = SimulatorUser, + AppId = appId, + RuleId = rule.Id, + Value = cronJob.Value, + }); + + yield break; + } + + var streamStart = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromDays(7)).ToDateTimeUtc(); + var streamFilter = StreamFilter.Prefix($"%-{appId.Id}"); + + await foreach (var storedEvent in eventStore.QueryAllReverseAsync(streamFilter, streamStart, MaxSimulatedEvents, ct)) + { + var @event = eventFormatter.ParseIfKnown(storedEvent); + + if (@event?.Payload is AppEvent) + { + yield return @event.To(); + } + } + } + public bool CanRunRule(Rule rule) { return rule.Trigger is not ManualTrigger; 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 0aec49f7e..19b8703bb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs @@ -7,13 +7,14 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Rules; +using Squidex.Flows.Internal; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Rules.Runner; public interface IRuleRunnerService { - Task> SimulateAsync(NamedId appId, DomainId ruleId, Rule rule, + Task> SimulateAsync(NamedId appId, RuleTrigger trigger, FlowDefinition flow, CancellationToken ct = default); Task> SimulateAsync(Rule rule, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleCronJobTriggered.cs similarity index 67% rename from backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleCronJobTriggered.cs index e029b0107..04d230a99 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleCronJobTriggered.cs @@ -5,13 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Rules; +using Squidex.Infrastructure.Json.Objects; -public enum RuleJobResult +namespace Squidex.Domain.Apps.Events.Rules; + +public sealed class RuleCronJobTriggered : RuleEvent { - Pending, - Success, - Retry, - Failed, - Cancelled, + public JsonValue Value { get; set; } } diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs index 043b0c3ae..07a1d31f2 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs @@ -166,7 +166,7 @@ public abstract partial class DomainObject : IAggregate where T : Entity, new protected virtual void RaiseEvent(Envelope @event) { - Guard.NotNull(@event, nameof(@event)); + Guard.NotNull(@event); @event.SetAggregateId(uniqueId); diff --git a/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs b/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs deleted file mode 100644 index 11c88ed91..000000000 --- a/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs +++ /dev/null @@ -1,105 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Globalization; -using System.Net; -using System.Net.Http.Headers; -using System.Text; - -namespace Squidex.Infrastructure.Http; - -public static class DumpFormatter -{ - public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? responseBody) - { - 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, TimeSpan elapsed, bool isTimeout = false) - { - var writer = new StringBuilder(); - - writer.AppendLine("Request:"); - writer.AppendRequest(request, requestBody); - - 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.AppendHeaders(request.Headers); - writer.AppendHeaders(request.Content?.Headers); - - if (!string.IsNullOrWhiteSpace(requestBody)) - { - writer.AppendLine(); - writer.AppendLine(requestBody); - } - } - - private static void AppendResponse(this StringBuilder writer, HttpResponseMessage? response, string? responseBody, TimeSpan elapsed, bool isTimeout) - { - 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}"); - } - } - - 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/Queries/OData/FilterVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs index b14a06461..3a4470a0f 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.Text.RegularExpressions; -using Google.Protobuf; using Microsoft.OData.UriParser; using Microsoft.Spatial; diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 133abddce..836f289f8 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -24,13 +24,13 @@ - - - - - - - + + + + + + + diff --git a/backend/src/Squidex.Shared/Texts.fr.resx b/backend/src/Squidex.Shared/Texts.fr.resx index dcea3809a..755c107d0 100644 --- a/backend/src/Squidex.Shared/Texts.fr.resx +++ b/backend/src/Squidex.Shared/Texts.fr.resx @@ -925,6 +925,12 @@ Login Test + + Cron Expression is invalid. + + + Timezone is not a valid IANA identifier. + Invalid next step ID @@ -937,6 +943,9 @@ Flow has no step + + Le schéma {id} n'existe pas. + La valeur par défaut calculée et la valeur par défaut ne peuvent pas être utilisées ensemble. @@ -964,9 +973,6 @@ Vous n'avez pas l'autorisation pour ce schéma. - - Le schéma {id} n'existe pas. - L'édition en ligne n'est pas autorisée pour l'éditeur radio. diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index c5da6446a..c303d6931 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -925,6 +925,12 @@ Login Test + + Cron Expression is invalid. + + + Timezone is not a valid IANA identifier. + Invalid next step ID @@ -937,6 +943,9 @@ Flow has no step + + Lo schema {id} non esiste. + Il valore predefinito calcolato e il valore predefinito non possono essere utilizzati insieme. @@ -964,9 +973,6 @@ Non hai i permessi per questo schema. - - Lo schema {id} non esiste. - Non è consentita per l'editor di tipo radio la modifica in linea. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index 01b2331c7..5c76f55b7 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -925,6 +925,12 @@ Login Test + + Cron Expression is invalid. + + + Timezone is not a valid IANA identifier. + Invalid next step ID @@ -937,6 +943,9 @@ Flow has no step + + Schema {id} bestaat niet. + Berekende standaardwaarde en standaardwaarde kunnen niet samen worden gebruikt. @@ -964,9 +973,6 @@ Je hebt geen toestemming voor dit schema. - - Schema {id} bestaat niet. - Inline bewerken is niet toegestaan ​​voor Radio-editor. diff --git a/backend/src/Squidex.Shared/Texts.pt.resx b/backend/src/Squidex.Shared/Texts.pt.resx index 3b92d20df..4648f8d8d 100644 --- a/backend/src/Squidex.Shared/Texts.pt.resx +++ b/backend/src/Squidex.Shared/Texts.pt.resx @@ -925,6 +925,12 @@ Login Test + + Cron Expression is invalid. + + + Timezone is not a valid IANA identifier. + Invalid next step ID @@ -937,6 +943,9 @@ Flow has no step + + Esquema {id} não existe. + Valor por defeito calculado e valor por defeito não podem ser usado em conjunto. @@ -964,9 +973,6 @@ Não tem permissões para este esquema. - - Esquema {id} não existe. - Não é permitido alteração em linha no Radio editor. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index 4c8fbbff0..3571182fc 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -925,6 +925,12 @@ Login Test + + Cron Expression is invalid. + + + Timezone is not a valid IANA identifier. + Invalid next step ID @@ -937,6 +943,9 @@ Flow has no step + + Schema {id} does not exist. + Calculated default value and default value cannot be used together. @@ -964,9 +973,6 @@ You do not have permission for this schema. - - Schema {id} does not exist. - Inline editing is not allowed for Radio editor. diff --git a/backend/src/Squidex.Shared/Texts.zh.resx b/backend/src/Squidex.Shared/Texts.zh.resx index b3a4fd467..69aed83fe 100644 --- a/backend/src/Squidex.Shared/Texts.zh.resx +++ b/backend/src/Squidex.Shared/Texts.zh.resx @@ -925,6 +925,12 @@ Login Test + + Cron Expression is invalid. + + + Timezone is not a valid IANA identifier. + Invalid next step ID @@ -937,6 +943,9 @@ Flow has no step + + Schema {id} does not exist. + 计算出的默认值和默认值不能一起使用。 @@ -964,9 +973,6 @@ You do not have permission for this schema. - - Schema {id} does not exist. - 无线电编辑器不允许内联编辑。 diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/DiscriminatorProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/DiscriminatorProcessor.cs index b43a0ce31..d098d8c7d 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/DiscriminatorProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/DiscriminatorProcessor.cs @@ -10,7 +10,6 @@ using NJsonSchema; using NJsonSchema.Generation; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Infrastructure.Reflection; -using System.Diagnostics; namespace Squidex.Areas.Api.Config.OpenApi; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index c7972db50..c4403403e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -30,6 +30,7 @@ public sealed class AssetsController( ICommandBus commandBus, IAssetQueryService assetQuery, ITagService tagService, + ScriptingCompleter scriptingCompleter, AssetTusRunner assetTusRunner) : ApiController(commandBus) { @@ -384,10 +385,9 @@ public sealed class AssetsController( [ApiPermissionOrAnonymous] [ApiCosts(1)] [ApiExplorerSettings(IgnoreApi = true)] - public IActionResult GetScriptCompletion(string app, string schema, - [FromServices] ScriptingCompleter completer) + public IActionResult GetScriptCompletion(string app, string schema) { - var completion = completer.AssetScript(); + var completion = scriptingCompleter.AssetScript(); return Ok(completion); } @@ -397,10 +397,9 @@ public sealed class AssetsController( [ApiPermissionOrAnonymous] [ApiCosts(1)] [ApiExplorerSettings(IgnoreApi = true)] - public IActionResult GetScriptTriggerCompletion(string app, string schema, - [FromServices] ScriptingCompleter completer) + public IActionResult GetScriptTriggerCompletion(string app, string schema) { - var completion = completer.AssetTrigger(); + var completion = scriptingCompleter.AssetTrigger(); return Ok(completion); } 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 589c9735b..e01771792 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 @@ -35,6 +35,11 @@ public sealed class RuleTriggerDtoFactory : IRuleTriggerVisitor public bool? IsEnabled { get; set; } = true; - public Rule ToRule() - { -#pragma warning disable CS0618 // Type or member is obsolete - return new Rule { Trigger = Trigger.ToTrigger(), Flow = GetFlow()! }; -#pragma warning restore CS0618 // Type or member is obsolete - } - public CreateRule ToCommand() { return SimpleMapper.Map(this, new CreateRule @@ -66,7 +58,7 @@ public sealed class CreateRuleDto } [Obsolete("Has been replaced by flows.")] - private FlowDefinition? GetFlow() + public FlowDefinition? GetFlow() { return Flow?.ToDefinition() ?? Action?.ToFlowDefinition(); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicUpdateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicUpdateRuleDto.cs index cb6bac7fd..d75853d9c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicUpdateRuleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicUpdateRuleDto.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Infrastructure.Validation; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Rules.Models; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/CronJobRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/CronJobRuleTriggerDto.cs new file mode 100644 index 000000000..c21b0a85e --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/CronJobRuleTriggerDto.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers; + +public sealed class CronJobRuleTriggerDto : RuleTriggerDto +{ + /// + /// The cron expression that defines the interval. + /// + [Required] + public string CronExpression { get; init; } + + /// + /// The optional timezone. + /// + public string? CronTimezone { get; init; } + + /// + /// The value sent to the flow. + /// + public JsonValue Value { get; init; } + + public static CronJobRuleTriggerDto FromDomain(CronJobTrigger trigger) + { + return SimpleMapper.Map(trigger, new CronJobRuleTriggerDto()); + } + + public override RuleTrigger ToTrigger() + { + return SimpleMapper.Map(this, new CronJobTrigger()); + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index 7edd7d8cf..e91a9aab3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Runner; using Squidex.Flows; +using Squidex.Flows.CronJobs; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; @@ -32,12 +33,14 @@ namespace Squidex.Areas.Api.Controllers.Rules; [ApiExplorerSettings(GroupName = nameof(Rules))] public sealed class RulesController( ICommandBus commandBus, + ICronJobManager cronJobs, IAppProvider appProvider, IFlowStepRegistry flowStepRegistry, IFlowManager flowManager, IRuleQueryService ruleQuery, IRuleRunnerService ruleRunnerService, IRuleValidator ruleValidator, + ScriptingCompleter scriptingCompleter, EventJsonSchemaGenerator eventJsonSchemaGenerator) : ApiController(commandBus) { @@ -312,8 +315,18 @@ public sealed class RulesController( [ApiCosts(5)] public async Task Simulate(string app, [FromBody] CreateRuleDto request) { - var simulated = request.ToRule(); - var simulation = await ruleRunnerService.SimulateAsync(App.NamedId(), DomainId.Empty, simulated, HttpContext.RequestAborted); +#pragma warning disable CS0618 // Type or member is obsolete + var flow = request.GetFlow(); +#pragma warning restore CS0618 // Type or member is obsolete + + if (flow == null) + { + return Ok(SimulatedRuleEventsDto.FromDomain([])); + } + + var simulation = + await ruleRunnerService.SimulateAsync(App.NamedId(), request.Trigger.ToTrigger(), flow, + HttpContext.RequestAborted); var response = SimulatedRuleEventsDto.FromDomain(simulation); @@ -495,14 +508,25 @@ public sealed class RulesController( [ApiPermissionOrAnonymous] [ApiCosts(1)] [ApiExplorerSettings(IgnoreApi = true)] - public IActionResult GetScriptCompletion(string app, string triggerType, - [FromServices] ScriptingCompleter completer) + public IActionResult GetScriptCompletion(string app, string triggerType) { - var completion = completer.Trigger(triggerType); + var completion = scriptingCompleter.Trigger(triggerType); return Ok(completion); } + [HttpGet] + [Route("apps/{app}/rules/timezones")] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + [ApiExplorerSettings(IgnoreApi = true)] + public IActionResult GetTimezones(string app) + { + var timezones = cronJobs.GetAvailableTimezoneIds(); + + return Ok(timezones); + } + private async Task InvokeCommandAsync(ICommand command) { var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 9db7689ef..dd878f999 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -27,7 +27,12 @@ namespace Squidex.Areas.Api.Controllers.Schemas; /// Update and query information about schemas. /// [ApiExplorerSettings(GroupName = nameof(Schemas))] -public sealed class SchemasController(ICommandBus commandBus, IAppProvider appProvider, IContentWorkflow workflow) : ApiController(commandBus) +public sealed class SchemasController( + ICommandBus commandBus, + IContentWorkflow workflow, + IAppProvider appProvider, + ScriptingCompleter scriptingCompleter) + : ApiController(commandBus) { /// /// Get schemas. @@ -307,10 +312,9 @@ public sealed class SchemasController(ICommandBus commandBus, IAppProvider appPr [ApiPermissionOrAnonymous] [ApiCosts(1)] [ApiExplorerSettings(IgnoreApi = true)] - public async Task GetContentScriptsCompletion(string app, string schema, - [FromServices] ScriptingCompleter completer) + public async Task GetContentScriptsCompletion(string app, string schema) { - var completion = completer.ContentScript(await BuildModel()); + var completion = scriptingCompleter.ContentScript(await BuildModel()); return Ok(completion); } @@ -320,10 +324,9 @@ public sealed class SchemasController(ICommandBus commandBus, IAppProvider appPr [ApiPermissionOrAnonymous] [ApiCosts(1)] [ApiExplorerSettings(IgnoreApi = true)] - public async Task GetContentTriggersCompletion(string app, string schema, - [FromServices] ScriptingCompleter completer) + public async Task GetContentTriggersCompletion(string app, string schema) { - var completion = completer.ContentTrigger(await BuildModel()); + var completion = scriptingCompleter.ContentTrigger(await BuildModel()); return Ok(completion); } @@ -333,10 +336,9 @@ public sealed class SchemasController(ICommandBus commandBus, IAppProvider appPr [ApiPermissionOrAnonymous] [ApiCosts(1)] [ApiExplorerSettings(IgnoreApi = true)] - public async Task GetFieldRulesCompletion(string app, string schema, - [FromServices] ScriptingCompleter completer) + public async Task GetFieldRulesCompletion(string app, string schema) { - var completion = completer.FieldRule(await BuildModel()); + var completion = scriptingCompleter.FieldRule(await BuildModel()); return Ok(completion); } @@ -346,10 +348,9 @@ public sealed class SchemasController(ICommandBus commandBus, IAppProvider appPr [ApiPermissionOrAnonymous] [ApiCosts(1)] [ApiExplorerSettings(IgnoreApi = true)] - public async Task GetPreviewUrlsCompletion(string app, string schema, - [FromServices] ScriptingCompleter completer) + public async Task GetPreviewUrlsCompletion(string app, string schema) { - var completion = completer.PreviewUrl(await BuildModel()); + var completion = scriptingCompleter.PreviewUrl(await BuildModel()); return Ok(completion); } diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index 222d57b35..7ba77facc 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -60,6 +60,9 @@ public static class RuleServices services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); @@ -81,6 +84,9 @@ public static class RuleServices services.AddSingletonAs() .As().As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .AsSelf(); @@ -112,5 +118,9 @@ public static class RuleServices .As>(); services.AddFlows(config); + services.AddCronJobs(config, m => + { + m.UpdateInterval = TimeSpan.FromSeconds(1); + }); } } diff --git a/backend/src/Squidex/Config/Messaging/MessagingServices.cs b/backend/src/Squidex/Config/Messaging/MessagingServices.cs index 6ef051573..8297a7856 100644 --- a/backend/src/Squidex/Config/Messaging/MessagingServices.cs +++ b/backend/src/Squidex/Config/Messaging/MessagingServices.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.Text.Json; -using Squidex.AI; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Subscriptions; using Squidex.Domain.Apps.Entities; @@ -71,6 +70,9 @@ public static class MessagingServices services.AddFlowsCore() .AddWorker(); + + services.AddCronJobsCore() + .AddWorker(); } if (isRandomName) diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 9e03fae1b..fa1669552 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -59,17 +59,17 @@ - - - - - - + + + + + + - - - - + + + + @@ -83,11 +83,11 @@ - + - + diff --git a/backend/tests/Squidex.Data.Tests.CodeGenerator/CodeGenerator.cs b/backend/tests/Squidex.Data.Tests.CodeGenerator/CodeGenerator.cs index 98a9a225b..b10b5be95 100644 --- a/backend/tests/Squidex.Data.Tests.CodeGenerator/CodeGenerator.cs +++ b/backend/tests/Squidex.Data.Tests.CodeGenerator/CodeGenerator.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Diagnostics; using System.Text; using HandlebarsDotNet; using Microsoft.CodeAnalysis; 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 945db9c33..a36dfd250 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 @@ -29,6 +29,7 @@ public class EventJsonSchemaGeneratorTests yield return nameof(EnrichedAssetEvent); yield return nameof(EnrichedCommentEvent); yield return nameof(EnrichedContentEvent); + yield return nameof(EnrichedCronJobEvent); yield return nameof(EnrichedManualEvent); yield return nameof(EnrichedSchemaEvent); yield return nameof(EnrichedUsageExceededEvent); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/CronJobTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/CronJobTriggerHandlerTests.cs new file mode 100644 index 000000000..d198141b5 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/CronJobTriggerHandlerTests.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Rules; + +public class CronJobTriggerHandlerTests +{ + private readonly IRuleTriggerHandler sut = new CronJobTriggerHandler(); + + [Fact] + public void Should_return_false_if_asking_for_snapshot_support() + { + Assert.False(sut.CanCreateSnapshotEvents); + } + + [Fact] + public void Should_calculate_name() + { + var @event = new RuleCronJobTriggered(); + + Assert.Equal("CronJob", sut.GetName(@event)); + } + + [Fact] + public async Task Should_create_event_with_name() + { + var @event = TestUtils.CreateEvent(); + var envelope = Envelope.Create(@event); + + var actual = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); + + var enrichedEvent = (EnrichedCronJobEvent)actual.Single(); + + 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"); + + var @event = new RuleCronJobTriggered { Actor = actor }; + var envelope = Envelope.Create(@event); + + var actual = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); + + Assert.Equal(actor, ((EnrichedUserEventBase)actual.Single()).Actor); + } + + [Fact] + public void Should_always_trigger() + { + var @event = new RuleCronJobTriggered(); + + Assert.True(sut.Trigger(Envelope.Create(@event), null!)); + } + + [Fact] + public void Should_always_trigger_enriched_event() + { + var @event = new EnrichedUsageExceededEvent(); + + Assert.True(sut.Trigger(@event, null!)); + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/CronJobUpdaterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/CronJobUpdaterTests.cs new file mode 100644 index 000000000..22fb8bb1c --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/CronJobUpdaterTests.cs @@ -0,0 +1,221 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Events; +using Squidex.Flows.CronJobs; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Rules; + +public sealed class CronJobUpdaterTests : GivenContext +{ + private readonly ICronJobManager cronJobs = A.Fake>(); + private readonly IRuleEnqueuer ruleEnqueuer = A.Fake(); + private readonly CronJobUpdater sut; + + public CronJobUpdaterTests() + { + sut = new CronJobUpdater(AppProvider, cronJobs, ruleEnqueuer); + } + + [Fact] + public void Should_return_rules_filter_for_events_filter() + { + Assert.Equal(StreamFilter.Prefix("rule-"), sut.EventsFilter); + } + + [Fact] + public async Task Should_register_handler_when_initialized() + { + await sut.InitializeAsync(default); + + A.CallTo(() => cronJobs.Subscribe(A, CancellationToken, Task>>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_register_new_cron_job() + { + var ruleId = DomainId.NewGuid(); + + var @event = + Envelope.Create( + new RuleCreated + { + AppId = AppId, + Trigger = new CronJobTrigger + { + CronExpression = "* */5 * * *", + CronTimezone = "Europe/Berlin", + }, + RuleId = ruleId, + }); + + await sut.On(@event); + + A.CallTo(() => cronJobs.AddAsync( + A>.That.Matches(x => + x.Id == ruleId.ToString() && + x.Context.RuleId == ruleId && + x.Context.AppId == AppId && + x.CronExpression == "* */5 * * *" && + x.CronTimezone == "Europe/Berlin"), + default)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_register_updated_cron_job() + { + var ruleId = DomainId.NewGuid(); + + var @event = + Envelope.Create( + new RuleUpdated + { + AppId = AppId, + Trigger = new CronJobTrigger + { + CronExpression = "* */5 * * *", + CronTimezone = "Europe/Berlin", + }, + RuleId = ruleId, + }); + + await sut.On(@event); + + A.CallTo(() => cronJobs.AddAsync( + A>.That.Matches(x => + x.Id == ruleId.ToString() && + x.Context.RuleId == ruleId && + x.Context.AppId == AppId && + x.CronExpression == "* */5 * * *" && + x.CronTimezone == "Europe/Berlin"), + default)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_unregister_when_trigger_changed() + { + var ruleId = DomainId.NewGuid(); + + var @event = + Envelope.Create( + new RuleUpdated + { + AppId = AppId, + Trigger = new ManualTrigger + { + }, + RuleId = ruleId, + }); + + await sut.On(@event); + + A.CallTo(() => cronJobs.RemoveAsync(ruleId.ToString(), default)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_unregister_cron_job_when_rule_deleted() + { + var ruleId = DomainId.NewGuid(); + + var @event = + Envelope.Create( + new RuleDeleted + { + RuleId = ruleId, + }); + + await sut.On(@event); + + A.CallTo(() => cronJobs.RemoveAsync(ruleId.ToString(), default)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_do_nothing_when_trigger_not_changed() + { + var ruleId = DomainId.NewGuid(); + + var @event = + Envelope.Create( + new RuleUpdated + { + RuleId = ruleId, + }); + + await sut.On(@event); + + A.CallTo(cronJobs) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_call_rule_enqueue_when_cron_job_due() + { + var rule = CreateAndSetupRule(new CronJobTrigger()); + + var job = new CronJob + { + Id = rule.Id.ToString(), + CronExpression = "* */5 * * *", + CronTimezone = "Europe/Berlin", + Context = new CronJobContext(AppId, rule.Id), + }; + + await sut.HandleCronJobAsync(job, CancellationToken); + + A.CallTo(() => ruleEnqueuer.EnqueueAsync(rule, A>._, CancellationToken)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_now_call_rule_enqueue_when_rule_has_no_cron_job_anymore() + { + var rule = CreateAndSetupRule(); + + var job = new CronJob + { + Id = rule.Id.ToString(), + CronExpression = "* */5 * * *", + CronTimezone = "Europe/Berlin", + Context = new CronJobContext(AppId, rule.Id), + }; + + await sut.HandleCronJobAsync(job, CancellationToken); + + A.CallTo(ruleEnqueuer) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_now_call_rule_enqueue_when_rule_is_not_found() + { + var rule = CreateAndSetupRule(); + + var job = new CronJob + { + Id = rule.Id.ToString(), + CronExpression = "* */5 * * *", + CronTimezone = "Europe/Berlin", + Context = new CronJobContext(AppId, rule.Id), + }; + + await sut.HandleCronJobAsync(job, CancellationToken); + + A.CallTo(ruleEnqueuer) + .MustNotHaveHappened(); + } +} 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 d38c9b66d..98bc0a586 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 @@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Flows; +using Squidex.Flows.CronJobs; using Squidex.Flows.Internal; using Squidex.Infrastructure.Validation; @@ -20,11 +21,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards; public class GuardRuleTests : GivenContext, IClassFixture { private readonly IFlowManager flowManager = A.Fake>(); + private readonly ICronJobManager cronJobs = A.Fake>(); private readonly IRuleValidator validator; public GuardRuleTests() { - validator = new RuleValidator(flowManager, AppProvider); + validator = new RuleValidator(flowManager, cronJobs, AppProvider); } [Fact] @@ -82,7 +84,7 @@ public class GuardRuleTests : GivenContext, IClassFixture { var command = new UpdateRule(); - await GuardRule.CanUpdate(command, CreateRule(), validator, CancellationToken); + await GuardRule.CanUpdate(command, validator, CancellationToken); } [Fact] @@ -90,7 +92,7 @@ public class GuardRuleTests : GivenContext, IClassFixture { var command = new UpdateRule { Name = "MyName" }; - await GuardRule.CanUpdate(command, CreateRule(), validator, CancellationToken); + await GuardRule.CanUpdate(command, validator, CancellationToken); } [Fact] @@ -109,7 +111,7 @@ public class GuardRuleTests : GivenContext, IClassFixture Name = "NewName", }); - await GuardRule.CanUpdate(command, CreateRule(), validator, CancellationToken); + await GuardRule.CanUpdate(command, validator, CancellationToken); } private T CreateCommand(T command) where T : IAppCommand 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 8b8a36a3e..a060bb7fc 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 @@ -22,7 +22,7 @@ public class ContentChangedTriggerTests : GivenContext, IClassFixture +{ + private readonly ICronJobManager cronJobs = A.Fake>(); + private readonly RuleValidator validator; + + public CronJobTriggerValidationTests() + { + A.CallTo(() => cronJobs.IsValidCronExpression(A._)) + .Returns(true); + + A.CallTo(() => cronJobs.IsValidTimezone(A._)) + .Returns(true); + + validator = new RuleValidator(null!, cronJobs, AppProvider); + } + + [Fact] + public async Task Should_add_error_if_expression_is_not_valid() + { + var trigger = new CronJobTrigger + { + CronExpression = "invalid", + }; + + A.CallTo(() => cronJobs.IsValidCronExpression("invalid")) + .Returns(false); + + var errors = await ValidateAsync(trigger); + + errors.Should().BeEquivalentTo( + [ + new ValidationError("Cron Expression is invalid.", "CronExpression"), + ]); + } + + [Fact] + public async Task Should_add_error_if_timezone_is_not_valid() + { + var trigger = new CronJobTrigger + { + CronTimezone = "invalid", + }; + + A.CallTo(() => cronJobs.IsValidTimezone("invalid")) + .Returns(false); + + var errors = await ValidateAsync(trigger); + + errors.Should().BeEquivalentTo( + [ + new ValidationError("Timezone is not a valid IANA identifier.", "CronTimezone"), + ]); + } + + [Fact] + public async Task Should_not_add_error_if_valid() + { + var trigger = new CronJobTrigger(); + + var errors = await ValidateAsync(trigger); + + Assert.Empty(errors); + } + + private async Task> ValidateAsync(RuleTrigger trigger) + { + var errors = new List(); + + await validator.ValidateTriggerAsync(trigger, AppId.Id, (m, p) => errors.Add(new ValidationError(m, p)), CancellationToken); + return 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 73e8d3741..caab7b4b2 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 @@ -19,7 +19,7 @@ public class UsageTriggerValidationTests : GivenContext, IClassFixture await VerifySutAsync(actual, None.Value); - A.CallTo(() => ruleEnqueuer.EnqueueAsync(sut.Snapshot.Id, sut.Snapshot, - A>.That.Matches(x => x.Payload is RuleManuallyTriggered))) + A.CallTo(() => ruleEnqueuer.EnqueueAsync( + sut.Snapshot, + A>.That.Matches(x => x.Payload is RuleManuallyTriggered), + A._)) .MustHaveHappened(); } 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 4d0217f52..73030914e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -76,12 +76,12 @@ public class RuleEnqueuerTests : GivenContext var rule = CreateAndSetupRule(); - A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), CancellationToken)) .Returns(Enumerable.Repeat(new JobResult(), 1).ToAsyncEnumerable()); - await sut.EnqueueAsync(rule.Id, rule, @event); + await sut.EnqueueAsync(rule, @event, CancellationToken); - A.CallTo(() => flowManager.EnqueueAsync(A[]>._, default)) + A.CallTo(() => flowManager.EnqueueAsync(A[]>._, CancellationToken)) .MustNotHaveHappened(); } @@ -112,10 +112,10 @@ public class RuleEnqueuerTests : GivenContext A.CallTo(() => flowManager.EnqueueAsync(A[]>._, default)) .Invokes(x => writes = x.GetArgument[]>(0)!); - A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), CancellationToken)) .Returns(new[] { result }.ToAsyncEnumerable()); - await sut.EnqueueAsync(rule.Id, rule, @event); + await sut.EnqueueAsync(rule, @event, CancellationToken); Assert.Equal(new[] { result.Job.Value }, writes); @@ -144,15 +144,15 @@ public class RuleEnqueuerTests : GivenContext }, }; - A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), CancellationToken)) .Returns(new[] { result }.ToAsyncEnumerable()); - await sut.EnqueueAsync(rule.Id, rule, @event); + await sut.EnqueueAsync(rule, @event, CancellationToken); A.CallTo(ruleUsageTracker) .MustNotHaveHappened(); - A.CallTo(() => flowManager.EnqueueAsync(A[]>._, default)) + A.CallTo(() => flowManager.EnqueueAsync(A[]>._, CancellationToken)) .MustNotHaveHappened(); } @@ -171,15 +171,15 @@ public class RuleEnqueuerTests : GivenContext Rule = rule, }; - A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), CancellationToken)) .Returns(new[] { result }.ToAsyncEnumerable()); - await sut.EnqueueAsync(rule.Id, rule, @event); + await sut.EnqueueAsync(rule, @event, CancellationToken); A.CallTo(ruleUsageTracker) .MustNotHaveHappened(); - A.CallTo(() => flowManager.EnqueueAsync(A[]>._, default)) + A.CallTo(() => flowManager.EnqueueAsync(A[]>._, CancellationToken)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleFlowTrackingCallbackTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleFlowTrackingCallbackTests.cs new file mode 100644 index 000000000..f484ad9a1 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleFlowTrackingCallbackTests.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Flows.Internal.Execution; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Rules; + +public class RuleFlowTrackingCallbackTests : GivenContext +{ + private readonly IRuleUsageTracker ruleUsageTracker = A.Fake(); + private readonly RuleFlowTrackingCallback sut; + + public RuleFlowTrackingCallbackTests() + { + sut = new RuleFlowTrackingCallback(ruleUsageTracker); + } + + [Fact] + public async Task Should_track_usage_with_success() + { + var ruleId = DomainId.NewGuid(); + + await sut.OnUpdateAsync( + new FlowExecutionState + { + InstanceId = default, + Context = new FlowEventContext(), + Definition = null!, + DefinitionId = ruleId.ToString(), + OwnerId = AppId.Id.ToString(), + Status = FlowExecutionStatus.Completed, + }, + CancellationToken); + + A.CallTo(() => ruleUsageTracker.TrackAsync( + AppId.Id, + ruleId, + A._, + 0, + 1, + 0, + CancellationToken)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_track_usage_with_failure() + { + var ruleId = DomainId.NewGuid(); + + await sut.OnUpdateAsync( + new FlowExecutionState + { + InstanceId = default, + Context = new FlowEventContext(), + Definition = null!, + DefinitionId = ruleId.ToString(), + OwnerId = AppId.Id.ToString(), + Status = FlowExecutionStatus.Failed, + }, + CancellationToken); + + A.CallTo(() => ruleUsageTracker.TrackAsync( + AppId.Id, + ruleId, + A._, + 0, + 0, + 1, + CancellationToken)) + .MustHaveHappened(); + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleValidatorTests.cs index 24fab1b4e..366db5986 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleValidatorTests.cs @@ -10,6 +10,7 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Flows; +using Squidex.Flows.CronJobs; using Squidex.Flows.Internal; using Squidex.Infrastructure.Validation; @@ -18,6 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Rules; public class RuleValidatorTests : GivenContext, IClassFixture { private readonly IFlowManager flowManager = A.Fake>(); + private readonly ICronJobManager cronJobs = A.Fake>(); private readonly RuleValidator sut; private sealed record TestFlowStep : FlowStep @@ -31,7 +33,7 @@ public class RuleValidatorTests : GivenContext, IClassFixture AppProvider.GetRulesAsync(AppId.Id, A._)) .Returns([rule]); @@ -226,7 +227,7 @@ public class GivenContext }; } - public EnrichedRule CreateRule() + public EnrichedRule CreateRule(RuleTrigger? trigger = null) { var id = DomainId.NewGuid(); @@ -240,7 +241,7 @@ public class GivenContext Name = "My Rule", LastModified = Timestamp(), LastModifiedBy = User, - Trigger = new ContentChangedTriggerV2(), + Trigger = trigger ?? new ContentChangedTriggerV2(), Version = 1, }; } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs deleted file mode 100644 index 2ba8c2b6e..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs +++ /dev/null @@ -1,127 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Net; -using System.Net.Http.Headers; -using System.Text; - -#pragma warning disable SA1122 // Use string.Empty for empty strings - -namespace Squidex.Infrastructure.Http; - -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) - { - return string.Join(Environment.NewLine, input) + Environment.NewLine; - } -} diff --git a/frontend/src/app/features/administration/pages/users/user-page.component.html b/frontend/src/app/features/administration/pages/users/user-page.component.html index e36056732..0524d61dc 100644 --- a/frontend/src/app/features/administration/pages/users/user-page.component.html +++ b/frontend/src/app/features/administration/pages/users/user-page.component.html @@ -39,7 +39,8 @@
- + +
@@ -51,7 +52,8 @@
- + + -
+
+ + +
+ + -
-

{{ "common.conditions" | sqxTranslate }}

- {{ "rules.conditionHint2" | sqxTranslate }} -
    -
  • - {{ "rules.conditions.event" | sqxTranslate }}:
    - event.type == 'Created' || event.type == 'Updated' -
  • +
    +

    {{ "common.conditions" | sqxTranslate }}

    + {{ "rules.conditionHint2" | sqxTranslate }} +
      +
    • + {{ "rules.conditions.event" | sqxTranslate }}:
      + event.type == 'Created' || event.type == 'Updated' +
    • -
    • - {{ "rules.conditions.largeAssets" | sqxTranslate }}:
      - event.fileSize > 100000000 -
    • +
    • + {{ "rules.conditions.largeAssets" | sqxTranslate }}:
      + event.fileSize > 100000000 +
    • -
    • - {{ "rules.conditions.images" | sqxTranslate }}:
      - event.isImage -
    • -
    +
  • + {{ "rules.conditions.images" | sqxTranslate }}:
    + event.isImage +
  • +
+
+
diff --git a/frontend/src/app/features/rules/shared/triggers/comment-trigger.component.html b/frontend/src/app/features/rules/shared/triggers/comment-trigger.component.html index 476450cb7..33b013fee 100644 --- a/frontend/src/app/features/rules/shared/triggers/comment-trigger.component.html +++ b/frontend/src/app/features/rules/shared/triggers/comment-trigger.component.html @@ -1,22 +1,26 @@
-
- - -
+
+ + +
+ + -
-

{{ "common.conditions" | sqxTranslate }}

- {{ "rules.conditionHint2" | sqxTranslate }} -
    -
  • - {{ "rules.conditions.commentUser" | sqxTranslate }}:
    - event.mentionedUser.email === 'mail2stehle@gmail.com' -
  • +
    +

    {{ "common.conditions" | sqxTranslate }}

    + {{ "rules.conditionHint2" | sqxTranslate }} +
      +
    • + {{ "rules.conditions.commentUser" | sqxTranslate }}:
      + event.mentionedUser.email === 'mail2stehle@gmail.com' +
    • -
    • - {{ "rules.conditions.commentKeyword" | sqxTranslate }}:
      - event.text.indexOf('urgent') >= 0 -
    • -
    +
  • + {{ "rules.conditions.commentKeyword" | sqxTranslate }}:
    + event.text.indexOf('urgent') >= 0 +
  • +
+
+
diff --git a/frontend/src/app/features/rules/shared/triggers/cron-job-trigger.component.html b/frontend/src/app/features/rules/shared/triggers/cron-job-trigger.component.html new file mode 100644 index 000000000..60df975cc --- /dev/null +++ b/frontend/src/app/features/rules/shared/triggers/cron-job-trigger.component.html @@ -0,0 +1,56 @@ +
+
+ + +
+ + + + + {{ "rules.conditions.cronExpressionHint" | sqxTranslate }} +
+ {{ "rules.readMore" | sqxTranslate }}: + + {{ "common.documentation" | sqxTranslate }} + +
+
+ +
+

{{ "rules.conditions.cronExpressionsTitle" | sqxTranslate }}

+ {{ "rules.conditions.cronExpressionsHint" | sqxTranslate }} +
    +
  • + {{ "rules.conditions.cronExpressionEvery4Hours" | sqxTranslate }}:
    + 0 */4 * * * +
  • + +
  • + {{ "rules.conditions.cronExpressionEveryMorning" | sqxTranslate }}:
    + 0 6 * * * +
  • + +
  • + {{ "rules.conditions.cronExpressionEveryMonth" | sqxTranslate }}:
    + 0 6 1 * * +
  • +
+
+
+
+ +
+ + +
+ + + {{ "rules.conditions.cronTimezoneHint" | sqxTranslate }} +
+
+
diff --git a/frontend/src/app/features/rules/shared/triggers/cron-job-trigger.component.scss b/frontend/src/app/features/rules/shared/triggers/cron-job-trigger.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/features/rules/shared/triggers/cron-job-trigger.component.ts b/frontend/src/app/features/rules/shared/triggers/cron-job-trigger.component.ts new file mode 100644 index 000000000..7e7c20a2e --- /dev/null +++ b/frontend/src/app/features/rules/shared/triggers/cron-job-trigger.component.ts @@ -0,0 +1,39 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { AsyncPipe } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { AppsState, CodeComponent, ControlErrorsComponent, FormHintComponent, RulesService, TranslatePipe, TriggerForm } from '@app/shared'; + +@Component({ + standalone: true, + selector: 'sqx-cron-job-trigger', + styleUrls: ['./cron-job-trigger.component.scss'], + templateUrl: './cron-job-trigger.component.html', + imports: [ + AsyncPipe, + CodeComponent, + ControlErrorsComponent, + FormHintComponent, + FormsModule, + ReactiveFormsModule, + TranslatePipe, + ], +}) +export class CronJobTriggerComponent { + @Input({ required: true }) + public triggerForm!: TriggerForm; + + public timezones = this.rulesService.getTimezones(this.appsState.appName); + + constructor( + private readonly appsState: AppsState, + private readonly rulesService: RulesService, + ) { + } +} diff --git a/frontend/src/app/features/rules/shared/triggers/schema-changed-trigger.component.html b/frontend/src/app/features/rules/shared/triggers/schema-changed-trigger.component.html index d34ae2a02..0852fddd2 100644 --- a/frontend/src/app/features/rules/shared/triggers/schema-changed-trigger.component.html +++ b/frontend/src/app/features/rules/shared/triggers/schema-changed-trigger.component.html @@ -1,22 +1,26 @@
-
- - -
+
+ + +
+ + -
-

{{ "common.conditions" | sqxTranslate }}

- {{ "rules.conditionHint2" | sqxTranslate }} -
    -
  • - {{ "rules.conditions.event" | sqxTranslate }}:
    - event.type == 'Created' || event.type == 'Updated' -
  • +
    +

    {{ "common.conditions" | sqxTranslate }}

    + {{ "rules.conditionHint2" | sqxTranslate }} +
      +
    • + {{ "rules.conditions.event" | sqxTranslate }}:
      + event.type == 'Created' || event.type == 'Updated' +
    • -
    • - {{ "rules.conditions.schema" | sqxTranslate }}:
      - schemaId.name === 'my-schema' -
    • -
    +
  • + {{ "rules.conditions.schema" | sqxTranslate }}:
    + schemaId.name === 'my-schema' +
  • +
+
+
diff --git a/frontend/src/app/features/rules/shared/triggers/usage-trigger.component.html b/frontend/src/app/features/rules/shared/triggers/usage-trigger.component.html index 339f34f3a..26adad6d8 100644 --- a/frontend/src/app/features/rules/shared/triggers/usage-trigger.component.html +++ b/frontend/src/app/features/rules/shared/triggers/usage-trigger.component.html @@ -1,14 +1,21 @@
-
- - - {{ "rules.conditions.usageLimitHint" | sqxTranslate }} +
+ + +
+ + + {{ "rules.conditions.usageLimitHint" | sqxTranslate }} +
-
- - - - {{ "rules.conditions.usageDaysHint" | sqxTranslate }} +
+ + +
+ + + {{ "rules.conditions.usageDaysHint" | sqxTranslate }} +
diff --git a/frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html b/frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html index b235674ab..8c0de90f7 100644 --- a/frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html +++ b/frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html @@ -9,13 +9,15 @@
- + + {{ "schemas.schemaLabelHint" | sqxTranslate }}
- + + {{ "schemas.schemaHintsHint" | sqxTranslate }}
@@ -28,25 +30,29 @@
- + + {{ "schemas.contentSidebarUrlHint" | sqxTranslate }}
- + + {{ "schemas.contentEditorUrlHint" | sqxTranslate }}
- + + {{ "schemas.contentsListUrlHint" | sqxTranslate }}
- + + {{ "schemas.schemaTagsHint" | sqxTranslate }}
diff --git a/frontend/src/app/features/settings/pages/more/more-page.component.html b/frontend/src/app/features/settings/pages/more/more-page.component.html index a87c9b7ae..2b7ceab5a 100644 --- a/frontend/src/app/features/settings/pages/more/more-page.component.html +++ b/frontend/src/app/features/settings/pages/more/more-page.component.html @@ -14,7 +14,8 @@
- + +
diff --git a/frontend/src/app/features/teams/pages/more/more-page.component.html b/frontend/src/app/features/teams/pages/more/more-page.component.html index 60da2ff72..eea967691 100644 --- a/frontend/src/app/features/teams/pages/more/more-page.component.html +++ b/frontend/src/app/features/teams/pages/more/more-page.component.html @@ -9,7 +9,8 @@
- + + {{ "teams.teamNameHint" | sqxTranslate }}
diff --git a/frontend/src/app/framework/angular/forms/editors/code-editor.component.html b/frontend/src/app/framework/angular/forms/editors/code-editor.component.html index 6c409e74e..1fe348ceb 100644 --- a/frontend/src/app/framework/angular/forms/editors/code-editor.component.html +++ b/frontend/src/app/framework/angular/forms/editors/code-editor.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/frontend/src/app/framework/angular/forms/editors/code-editor.component.scss b/frontend/src/app/framework/angular/forms/editors/code-editor.component.scss index d4c6f4db8..7db24be28 100644 --- a/frontend/src/app/framework/angular/forms/editors/code-editor.component.scss +++ b/frontend/src/app/framework/angular/forms/editors/code-editor.component.scss @@ -6,8 +6,6 @@ :host ::ng-deep { .ace_editor { background: $color-white; - border: 1px solid $color-input; - border-radius: 0; &.no-border { border: 0; @@ -39,7 +37,7 @@ } .ace_gutter { - background: $color-border !important; + bottom: -1px; } .ace_bracket { @@ -68,4 +66,8 @@ display: none; } } +} + +.editor-container { + border: 1px solid $color-input; } \ No newline at end of file diff --git a/frontend/src/app/shared/components/assets/asset-dialog.component.html b/frontend/src/app/shared/components/assets/asset-dialog.component.html index af61fdf84..e97c48735 100644 --- a/frontend/src/app/shared/components/assets/asset-dialog.component.html +++ b/frontend/src/app/shared/components/assets/asset-dialog.component.html @@ -137,12 +137,14 @@
- + +
- + +