diff --git a/backend/i18n/frontend_de.json b/backend/i18n/frontend_de.json index eee6a426d7..2b033dea4f 100644 --- a/backend/i18n/frontend_de.json +++ b/backend/i18n/frontend_de.json @@ -191,6 +191,10 @@ "clients.empty": "Noch kein Client erstellt.", "clients.loadFailed": "Fehler beim Laden der Clients. Bitte neu laden.", "clients.refreshTooltip": "Clients aktualisieren", + "clients.regenerateSecret": "Regenerate Client Secret", + "clients.regenerateSecretConfirmText": "Do you really want to regenerate the secret? Applications that use the current secret cannot request new access tokens anymore.", + "clients.regenerateSecretConfirmTitle": "Regenerate secret", + "clients.regenerateSecretFailed": "Failed to regenerate client secret. Please reload.", "clients.reloaded": "Clients neu geladen.", "clients.revokeFailed": "Fehler beim Widerrufen des Clients. Bitte neu laden.", "clients.tokenFailed": "Fehler beim Erstellen des Tokens. Bitte versuchen Sie es erneut.", @@ -390,6 +394,7 @@ "common.saveEdit": "Speichern & bearbeiten", "common.schema": "Schema", "common.schemas": "Schemas", + "common.scriptLogs": "Script Logs", "common.search": "Suchen", "common.searchGoogleMaps": "Google Maps durchsuchen", "common.searchResults": "Suchergebnisse", @@ -1131,6 +1136,13 @@ "schemas.updateUIFieldsFailed": "Fehler beim Aktualisieren der UI-Felder. Bitte neu laden.", "schemas.validateOnPublish": "Beim Veröffentlichen validieren", "schemas.validateOnPublishHint": "Verwenden Sie benutzerdefinierte Workflows, wenn Sie mehr Kontrolle darüber benötigen, für welche Status ein Inhalt validiert werden soll.", + "scriptLogs.description": "The console output of content and asset scripts. Only the latest logs are kept.", + "scriptLogs.empty": "No script logs found.", + "scriptLogs.filterPlaceholder": "Filter by script, e.g. contents/my-schema", + "scriptLogs.loadFailed": "Failed to load script logs. Please reload.", + "scriptLogs.omitted": "{count} more entries have been omitted.", + "scriptLogs.refreshTooltip": "Refresh script logs", + "scriptLogs.reloaded": "Script logs reloaded.", "search.addFilter": "Filter hinzufügen", "search.addGroup": "Gruppe hinzufügen", "search.addSorting": "Sortierung hinzufügen", diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 3532565041..dd054103c7 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -191,6 +191,10 @@ "clients.empty": "No client created yet.", "clients.loadFailed": "Failed to load clients. Please reload.", "clients.refreshTooltip": "Refresh clients", + "clients.regenerateSecret": "Regenerate Client Secret", + "clients.regenerateSecretConfirmText": "Do you really want to regenerate the secret? Applications that use the current secret cannot request new access tokens anymore.", + "clients.regenerateSecretConfirmTitle": "Regenerate secret", + "clients.regenerateSecretFailed": "Failed to regenerate client secret. Please reload.", "clients.reloaded": "Clients reloaded.", "clients.revokeFailed": "Failed to revoke client. Please reload.", "clients.tokenFailed": "Failed to create token. Please retry.", @@ -390,6 +394,7 @@ "common.saveEdit": "Save & editor", "common.schema": "Schema", "common.schemas": "Schemas", + "common.scriptLogs": "Script Logs", "common.search": "Search", "common.searchGoogleMaps": "Search Google Maps", "common.searchResults": "Search Results", @@ -1131,6 +1136,13 @@ "schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.", "schemas.validateOnPublish": "Validate when publishing", "schemas.validateOnPublishHint": "Use custom workflows when you need more control for which statuses a content should be validated.", + "scriptLogs.description": "The console output of content and asset scripts. Only the latest logs are kept.", + "scriptLogs.empty": "No script logs found.", + "scriptLogs.filterPlaceholder": "Filter by script, e.g. contents/my-schema", + "scriptLogs.loadFailed": "Failed to load script logs. Please reload.", + "scriptLogs.omitted": "{count} more entries have been omitted.", + "scriptLogs.refreshTooltip": "Refresh script logs", + "scriptLogs.reloaded": "Script logs reloaded.", "search.addFilter": "Add Filter", "search.addGroup": "Add Group", "search.addSorting": "Add Sorting", diff --git a/backend/i18n/frontend_fr.json b/backend/i18n/frontend_fr.json index 5564d68204..9eb2ce5bd7 100644 --- a/backend/i18n/frontend_fr.json +++ b/backend/i18n/frontend_fr.json @@ -191,6 +191,10 @@ "clients.empty": "Aucun client créé pour le moment.", "clients.loadFailed": "Échec du chargement des clients. Veuillez recharger.", "clients.refreshTooltip": "Actualiser les clients", + "clients.regenerateSecret": "Regenerate Client Secret", + "clients.regenerateSecretConfirmText": "Do you really want to regenerate the secret? Applications that use the current secret cannot request new access tokens anymore.", + "clients.regenerateSecretConfirmTitle": "Regenerate secret", + "clients.regenerateSecretFailed": "Failed to regenerate client secret. Please reload.", "clients.reloaded": "Clients rechargés.", "clients.revokeFailed": "Échec de la révocation du client. Veuillez recharger.", "clients.tokenFailed": "Échec de la création du jeton. Veuillez réessayer.", @@ -390,6 +394,7 @@ "common.saveEdit": "Save & editor", "common.schema": "Schéma", "common.schemas": "Schémas", + "common.scriptLogs": "Script Logs", "common.search": "Recherche", "common.searchGoogleMaps": "Rechercher Google Maps", "common.searchResults": "Résultats de recherche", @@ -1131,6 +1136,13 @@ "schemas.updateUIFieldsFailed": "Échec de la mise à jour des champs de l'interface utilisateur. Veuillez recharger.", "schemas.validateOnPublish": "Valider lors de la publication", "schemas.validateOnPublishHint": "Utilisez des flux de travail personnalisés lorsque vous avez besoin de plus de contrôle sur les statuts pour lesquels un contenu doit être validé.", + "scriptLogs.description": "The console output of content and asset scripts. Only the latest logs are kept.", + "scriptLogs.empty": "No script logs found.", + "scriptLogs.filterPlaceholder": "Filter by script, e.g. contents/my-schema", + "scriptLogs.loadFailed": "Failed to load script logs. Please reload.", + "scriptLogs.omitted": "{count} more entries have been omitted.", + "scriptLogs.refreshTooltip": "Refresh script logs", + "scriptLogs.reloaded": "Script logs reloaded.", "search.addFilter": "Ajouter un filtre", "search.addGroup": "Ajouter un groupe", "search.addSorting": "Ajouter un tri", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index aa01749249..c7d2f25357 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -191,6 +191,10 @@ "clients.empty": "Nessun client ancora creato.", "clients.loadFailed": "Non è stato possibile caricare i client. Per favore ricarica.", "clients.refreshTooltip": "Aggiorna i client", + "clients.regenerateSecret": "Regenerate Client Secret", + "clients.regenerateSecretConfirmText": "Do you really want to regenerate the secret? Applications that use the current secret cannot request new access tokens anymore.", + "clients.regenerateSecretConfirmTitle": "Regenerate secret", + "clients.regenerateSecretFailed": "Failed to regenerate client secret. Please reload.", "clients.reloaded": "Client ricaricati.", "clients.revokeFailed": "Non è stato possibile rimuovere il client. Per favore ricarica.", "clients.tokenFailed": "Non è stato possibile creare il token. Per favore riprova.", @@ -390,6 +394,7 @@ "common.saveEdit": "Save & editor", "common.schema": "Schema", "common.schemas": "Schemi", + "common.scriptLogs": "Script Logs", "common.search": "Search", "common.searchGoogleMaps": "Cerca su Google Maps", "common.searchResults": "Risultati di ricerca", @@ -1131,6 +1136,13 @@ "schemas.updateUIFieldsFailed": "Non è stato possibile aggiornare i campi della UI. Per favore ricarica.", "schemas.validateOnPublish": "Valida quando pubblichi", "schemas.validateOnPublishHint": "Use custom workflows when you need more control for which statuses a content should be validated.", + "scriptLogs.description": "The console output of content and asset scripts. Only the latest logs are kept.", + "scriptLogs.empty": "No script logs found.", + "scriptLogs.filterPlaceholder": "Filter by script, e.g. contents/my-schema", + "scriptLogs.loadFailed": "Failed to load script logs. Please reload.", + "scriptLogs.omitted": "{count} more entries have been omitted.", + "scriptLogs.refreshTooltip": "Refresh script logs", + "scriptLogs.reloaded": "Script logs reloaded.", "search.addFilter": "Aggiungi un Filtro", "search.addGroup": "Aggiungi un Gruppo", "search.addSorting": "Aggiungi ordinamento", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index e8dfba5538..2b6f9072ca 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -191,6 +191,10 @@ "clients.empty": "Nog geen client aangemaakt.", "clients.loadFailed": "Kan clients niet laden. Laad opnieuw.", "clients.refreshTooltip": "Ververs clients", + "clients.regenerateSecret": "Regenerate Client Secret", + "clients.regenerateSecretConfirmText": "Do you really want to regenerate the secret? Applications that use the current secret cannot request new access tokens anymore.", + "clients.regenerateSecretConfirmTitle": "Regenerate secret", + "clients.regenerateSecretFailed": "Failed to regenerate client secret. Please reload.", "clients.reloaded": "Clients herladen.", "clients.revokeFailed": "Kan client niet intrekken. Laad opnieuw.", "clients.tokenFailed": "Maken van token is mislukt. Probeer het opnieuw.", @@ -390,6 +394,7 @@ "common.saveEdit": "Save & editor", "common.schema": "Schema", "common.schemas": "Schema's", + "common.scriptLogs": "Script Logs", "common.search": "Zoeken", "common.searchGoogleMaps": "Zoeken in Google Maps", "common.searchResults": "Zoekresultaten", @@ -1131,6 +1136,13 @@ "schemas.updateUIFieldsFailed": "Bijwerken van UI-velden is mislukt. Laad opnieuw.", "schemas.validateOnPublish": "Valideren bij het publiceren", "schemas.validateOnPublishHint": "Use custom workflows when you need more control for which statuses a content should be validated.", + "scriptLogs.description": "The console output of content and asset scripts. Only the latest logs are kept.", + "scriptLogs.empty": "No script logs found.", + "scriptLogs.filterPlaceholder": "Filter by script, e.g. contents/my-schema", + "scriptLogs.loadFailed": "Failed to load script logs. Please reload.", + "scriptLogs.omitted": "{count} more entries have been omitted.", + "scriptLogs.refreshTooltip": "Refresh script logs", + "scriptLogs.reloaded": "Script logs reloaded.", "search.addFilter": "Filter toevoegen", "search.addGroup": "Groep toevoegen", "search.addSorting": "Sortering toevoegen", diff --git a/backend/i18n/frontend_pt.json b/backend/i18n/frontend_pt.json index f918b9d5b9..58c72caf85 100644 --- a/backend/i18n/frontend_pt.json +++ b/backend/i18n/frontend_pt.json @@ -191,6 +191,10 @@ "clients.empty": "Nenhum cliente foi criado ainda.", "clients.loadFailed": "Falhou em carregar clientes. Por favor, recarregue.", "clients.refreshTooltip": "Atualizar clientes", + "clients.regenerateSecret": "Regenerate Client Secret", + "clients.regenerateSecretConfirmText": "Do you really want to regenerate the secret? Applications that use the current secret cannot request new access tokens anymore.", + "clients.regenerateSecretConfirmTitle": "Regenerate secret", + "clients.regenerateSecretFailed": "Failed to regenerate client secret. Please reload.", "clients.reloaded": "Clientes recarregados.", "clients.revokeFailed": "Falhou em revogar o cliente. Por favor, recarregue.", "clients.tokenFailed": "Falhou em criar um símbolo. Por favor, reda o redando.", @@ -390,6 +394,7 @@ "common.saveEdit": "Save & editor", "common.schema": "Esquema", "common.schemas": "Esquemas", + "common.scriptLogs": "Script Logs", "common.search": "Pesquisar", "common.searchGoogleMaps": "Pesquisar Google Maps", "common.searchResults": "Resultados da pesquisa", @@ -1131,6 +1136,13 @@ "schemas.updateUIFieldsFailed": "Falhou na atualização dos campos de UI. Por favor, recarregue.", "schemas.validateOnPublish": "Validar ao publicar", "schemas.validateOnPublishHint": "Utilize fluxos de trabalho personalizados quando necessitar de mais controlo para os estatutos que um conteúdo deve ser validado.", + "scriptLogs.description": "The console output of content and asset scripts. Only the latest logs are kept.", + "scriptLogs.empty": "No script logs found.", + "scriptLogs.filterPlaceholder": "Filter by script, e.g. contents/my-schema", + "scriptLogs.loadFailed": "Failed to load script logs. Please reload.", + "scriptLogs.omitted": "{count} more entries have been omitted.", + "scriptLogs.refreshTooltip": "Refresh script logs", + "scriptLogs.reloaded": "Script logs reloaded.", "search.addFilter": "Adicionar filtro", "search.addGroup": "Adicionar Grupo", "search.addSorting": "Adicionar Classificação", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 27fc0c3790..aaa8640d7f 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -191,6 +191,10 @@ "clients.empty": "尚未创建客户端。", "clients.loadFailed": "加载客户端失败。请重新加载。", "clients.refreshTooltip": "刷新客户端", + "clients.regenerateSecret": "Regenerate Client Secret", + "clients.regenerateSecretConfirmText": "Do you really want to regenerate the secret? Applications that use the current secret cannot request new access tokens anymore.", + "clients.regenerateSecretConfirmTitle": "Regenerate secret", + "clients.regenerateSecretFailed": "Failed to regenerate client secret. Please reload.", "clients.reloaded": "客户端重新加载。", "clients.revokeFailed": "撤销客户端失败。请重新加载。", "clients.tokenFailed": "创建令牌失败。请重试。", @@ -390,6 +394,7 @@ "common.saveEdit": "Save & editor", "common.schema": "Schema", "common.schemas": "Schemas", + "common.scriptLogs": "Script Logs", "common.search": "搜索", "common.searchGoogleMaps": "搜索谷歌地图", "common.searchResults": "搜索结果", @@ -1131,6 +1136,13 @@ "schemas.updateUIFieldsFailed": "无法更新 UI 字段。请重新加载。", "schemas.validateOnPublish": "发布时验证", "schemas.validateOnPublishHint": "Use custom workflows when you need more control for which statuses a content should be validated.", + "scriptLogs.description": "The console output of content and asset scripts. Only the latest logs are kept.", + "scriptLogs.empty": "No script logs found.", + "scriptLogs.filterPlaceholder": "Filter by script, e.g. contents/my-schema", + "scriptLogs.loadFailed": "Failed to load script logs. Please reload.", + "scriptLogs.omitted": "{count} more entries have been omitted.", + "scriptLogs.refreshTooltip": "Refresh script logs", + "scriptLogs.reloaded": "Script logs reloaded.", "search.addFilter": "添加过滤器", "search.addGroup": "添加组", "search.addSorting": "添加排序", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index be1ae1ea58..dc53b2771b 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -217,6 +217,7 @@ "history.apps.assetScriptsConfigured": "updated asset scripts", "history.apps.clientAdded": "added client {[Id]} to app", "history.apps.clientRevoked": "revoked client {[Id]}", + "history.apps.clientSecretRegenerated": "regenerated secret of client {[Id]}", "history.apps.clientUpdated": "updated client {[Id]}", "history.apps.common.updated": "updated general settings", "history.apps.contributoreAssigned": "assigned {user:[Contributor]} as {[Role]}", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 3532565041..dd054103c7 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -191,6 +191,10 @@ "clients.empty": "No client created yet.", "clients.loadFailed": "Failed to load clients. Please reload.", "clients.refreshTooltip": "Refresh clients", + "clients.regenerateSecret": "Regenerate Client Secret", + "clients.regenerateSecretConfirmText": "Do you really want to regenerate the secret? Applications that use the current secret cannot request new access tokens anymore.", + "clients.regenerateSecretConfirmTitle": "Regenerate secret", + "clients.regenerateSecretFailed": "Failed to regenerate client secret. Please reload.", "clients.reloaded": "Clients reloaded.", "clients.revokeFailed": "Failed to revoke client. Please reload.", "clients.tokenFailed": "Failed to create token. Please retry.", @@ -390,6 +394,7 @@ "common.saveEdit": "Save & editor", "common.schema": "Schema", "common.schemas": "Schemas", + "common.scriptLogs": "Script Logs", "common.search": "Search", "common.searchGoogleMaps": "Search Google Maps", "common.searchResults": "Search Results", @@ -1131,6 +1136,13 @@ "schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.", "schemas.validateOnPublish": "Validate when publishing", "schemas.validateOnPublishHint": "Use custom workflows when you need more control for which statuses a content should be validated.", + "scriptLogs.description": "The console output of content and asset scripts. Only the latest logs are kept.", + "scriptLogs.empty": "No script logs found.", + "scriptLogs.filterPlaceholder": "Filter by script, e.g. contents/my-schema", + "scriptLogs.loadFailed": "Failed to load script logs. Please reload.", + "scriptLogs.omitted": "{count} more entries have been omitted.", + "scriptLogs.refreshTooltip": "Refresh script logs", + "scriptLogs.reloaded": "Script logs reloaded.", "search.addFilter": "Add Filter", "search.addGroup": "Add Group", "search.addSorting": "Add Sorting", diff --git a/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs b/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs index bcb4a16ebc..4c55146f8c 100644 --- a/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/AppDbContext.cs @@ -59,6 +59,7 @@ protected override void OnModelCreating(ModelBuilder builder) builder.UseRequest(jsonSerializer, jsonColumnType); builder.UseRules(jsonSerializer, jsonColumnType); builder.UseSchema(jsonSerializer, jsonColumnType); + builder.UseScriptLogs(jsonSerializer, jsonColumnType); builder.UseSettings(jsonSerializer, jsonColumnType); builder.UseTags(jsonSerializer, jsonColumnType); builder.UseTeams(jsonSerializer, jsonColumnType); diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Scripting/EFScriptLogBuilder.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Scripting/EFScriptLogBuilder.cs new file mode 100644 index 0000000000..4d3876549d --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Scripting/EFScriptLogBuilder.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; + +namespace Microsoft.EntityFrameworkCore; + +public static class EFScriptLogBuilder +{ + public static void UseScriptLogs(this ModelBuilder builder, IJsonSerializer jsonSerializer, string? jsonColumn) + { + builder.Entity(b => + { + b.ToTable("ScriptLogs"); + b.HasKey(x => x.Id); + b.HasIndex(x => new { x.AppId, x.Timestamp }); + b.HasIndex(x => x.Timestamp); + b.Property(x => x.Id).AsString(); + b.Property(x => x.AppId).AsString(); + b.Property(x => x.Name).HasMaxLength(255); + b.Property(x => x.Entries).AsJsonString(jsonSerializer, jsonColumn); + b.Property(x => x.Timestamp).AsDateTimeOffset(); + }); + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Scripting/EFScriptLogRepository.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Scripting/EFScriptLogRepository.cs new file mode 100644 index 0000000000..dae1c204cd --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Scripting/EFScriptLogRepository.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.EntityFrameworkCore; +using NodaTime; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Scripting.Repositories; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Scripting; + +public sealed class EFScriptLogRepository(IDbContextFactory dbContextFactory) + : IScriptLogRepository where TContext : DbContext +{ + public async Task InsertManyAsync(IEnumerable records, + CancellationToken ct = default) + { + Guard.NotNull(records); + + var entities = records.ToList(); + if (entities.Count == 0) + { + return; + } + + await using var dbContext = await CreateDbContextAsync(ct); + await dbContext.BulkInsertAsync(entities, ct); + } + + public async Task TrimAsync(DomainId appId, int maxCount, + CancellationToken ct = default) + { + await using var dbContext = await CreateDbContextAsync(ct); + + // Find the newest record that exceeds the limit. Everything that is not newer is deleted. + var boundary = + await dbContext.Set() + .Where(x => x.AppId == appId) + .OrderByDescending(x => x.Timestamp) + .Skip(maxCount) + .Take(1) + .Select(x => x.Timestamp) + .ToListAsync(ct); + + if (boundary.Count == 0) + { + return; + } + + var timestamp = boundary[0]; + + await dbContext.Set().Where(x => x.AppId == appId && x.Timestamp <= timestamp) + .ExecuteDeleteAsync(ct); + } + + public async Task DeleteAsync(DomainId appId, + CancellationToken ct = default) + { + await using var dbContext = await CreateDbContextAsync(ct); + + await dbContext.Set().Where(x => x.AppId == appId) + .ExecuteDeleteAsync(ct); + } + + public async Task DeleteOlderThanAsync(Instant timestamp, + CancellationToken ct = default) + { + await using var dbContext = await CreateDbContextAsync(ct); + + await dbContext.Set().Where(x => x.Timestamp < timestamp) + .ExecuteDeleteAsync(ct); + } + + public async Task> QueryAsync(DomainId appId, string? name, int skip, int take, + CancellationToken ct = default) + { + await using var dbContext = await CreateDbContextAsync(ct); + + var query = dbContext.Set().Where(x => x.AppId == appId); + + if (!string.IsNullOrWhiteSpace(name)) + { + // The name is a path, therefore the filter matches all logs in the same group. + // The overload with a comparison type cannot be translated to SQL. +#pragma warning disable RECS0063 // Warns when a culture-aware 'StartsWith' call is used by default. + query = query.Where(x => x.Name.StartsWith(name)); +#pragma warning restore RECS0063 // Warns when a culture-aware 'StartsWith' call is used by default. + } + + var result = + await query + .OrderByDescending(x => x.Timestamp) + .Skip(skip) + .Take(take) + .ToListAsync(ct); + + return result; + } + + private Task CreateDbContextAsync(CancellationToken ct) + { + return dbContextFactory.CreateDbContextAsync(ct); + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260921120832_AddScriptLogs.Designer.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260921120832_AddScriptLogs.Designer.cs new file mode 100644 index 0000000000..cf22762557 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260921120832_AddScriptLogs.Designer.cs @@ -0,0 +1,1665 @@ +// +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("20260921120832_AddScriptLogs")] + partial class AddScriptLogs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.6") + .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") + .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(150) + .HasColumnType("varchar(150)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type") + .HasAnnotation("MySql:IndexPrefixLength", new[] { 0, 0, 0, 50 }); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Squidex.AI.EntityFramework.EFChatEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("LastUpdated") + .HasColumnType("datetime(6)"); + + b.Property("LastVersion") + .IsConcurrencyToken() + .HasColumnType("char(36)") + .HasColumnName("Version"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + 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.Core.Scripting.ScriptLogRecord", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Entries") + .IsRequired() + .HasColumnType("json"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.Property("TotalEntries") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Timestamp"); + + b.HasIndex("AppId", "Timestamp"); + + b.ToTable("ScriptLogs", (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") + .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") + .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"); + }); + + 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"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexUserInfoEntity", 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("UserInfoApiKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserInfoRole") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("UserInfoApiKey"); + + b.ToTable("UserInfos"); + }); + + 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") + .HasColumnType("char(36)"); + + b.Property("EventStream") + .IsRequired() + .HasMaxLength(750) + .HasColumnType("varchar(750)"); + + b.Property("EventStreamOffset") + .HasColumnType("bigint"); + + b.PrimitiveCollection("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") + .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/20260921120832_AddScriptLogs.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260921120832_AddScriptLogs.cs new file mode 100644 index 0000000000..d83d839015 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260921120832_AddScriptLogs.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Squidex.Providers.MySql.App.Migrations +{ + /// + public partial class AddScriptLogs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ScriptLogs", + columns: table => new + { + Id = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + AppId = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Name = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Timestamp = table.Column(type: "datetime(6)", nullable: false), + Entries = table.Column(type: "json", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TotalEntries = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ScriptLogs", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_ScriptLogs_AppId_Timestamp", + table: "ScriptLogs", + columns: new[] { "AppId", "Timestamp" }); + + migrationBuilder.CreateIndex( + name: "IX_ScriptLogs_Timestamp", + table: "ScriptLogs", + column: "Timestamp"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ScriptLogs"); + } + } +} 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 615ca9a142..79a6d994a3 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 @@ -367,6 +367,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AssetKeyValueStore_TusMetadata", (string)null); }); + modelBuilder.Entity("Squidex.Domain.Apps.Core.Scripting.ScriptLogRecord", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Entries") + .IsRequired() + .HasColumnType("json"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.Property("TotalEntries") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Timestamp"); + + b.HasIndex("AppId", "Timestamp"); + + b.ToTable("ScriptLogs", (string)null); + }); + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Apps.EFAppEntity", b => { b.Property("DocumentId") diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260921120842_AddScriptLogs.Designer.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260921120842_AddScriptLogs.Designer.cs new file mode 100644 index 0000000000..513eec08b0 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260921120842_AddScriptLogs.Designer.cs @@ -0,0 +1,1665 @@ +// +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("20260921120842_AddScriptLogs")] + partial class AddScriptLogs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.6") + .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") + .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(150) + .HasColumnType("character varying(150)"); + + 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.EntityFramework.EFChatEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("LastVersion") + .IsConcurrencyToken() + .HasColumnType("uuid") + .HasColumnName("Version"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + 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.Core.Scripting.ScriptLogRecord", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Entries") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalEntries") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Timestamp"); + + b.HasIndex("AppId", "Timestamp"); + + b.ToTable("ScriptLogs", (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") + .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") + .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"); + }); + + 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"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexUserInfoEntity", 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("UserInfoApiKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserInfoRole") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("UserInfoApiKey"); + + b.ToTable("UserInfos"); + }); + + 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") + .HasColumnType("uuid"); + + b.Property("EventStream") + .IsRequired() + .HasMaxLength(750) + .HasColumnType("character varying(750)"); + + b.Property("EventStreamOffset") + .HasColumnType("bigint"); + + b.PrimitiveCollection("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") + .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/20260921120842_AddScriptLogs.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260921120842_AddScriptLogs.cs new file mode 100644 index 0000000000..58d0b1d7ff --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260921120842_AddScriptLogs.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Squidex.Providers.Postgres.App.Migrations +{ + /// + public partial class AddScriptLogs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ScriptLogs", + columns: table => new + { + Id = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + AppId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false), + Entries = table.Column(type: "jsonb", nullable: false), + TotalEntries = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ScriptLogs", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ScriptLogs_AppId_Timestamp", + table: "ScriptLogs", + columns: new[] { "AppId", "Timestamp" }); + + migrationBuilder.CreateIndex( + name: "IX_ScriptLogs_Timestamp", + table: "ScriptLogs", + column: "Timestamp"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ScriptLogs"); + } + } +} 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 35d60a044d..13631a53ea 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 @@ -367,6 +367,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AssetKeyValueStore_TusMetadata", (string)null); }); + modelBuilder.Entity("Squidex.Domain.Apps.Core.Scripting.ScriptLogRecord", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Entries") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalEntries") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Timestamp"); + + b.HasIndex("AppId", "Timestamp"); + + b.ToTable("ScriptLogs", (string)null); + }); + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Apps.EFAppEntity", b => { b.Property("DocumentId") diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260921120852_AddScriptLogs.Designer.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260921120852_AddScriptLogs.Designer.cs new file mode 100644 index 0000000000..ca6671329c --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260921120852_AddScriptLogs.Designer.cs @@ -0,0 +1,1667 @@ +// +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("20260921120852_AddScriptLogs")] + partial class AddScriptLogs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.6") + .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") + .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(150) + .HasColumnType("nvarchar(150)"); + + 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.EntityFramework.EFChatEntity", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("LastUpdated") + .HasColumnType("datetime2"); + + b.Property("LastVersion") + .IsConcurrencyToken() + .HasColumnType("uniqueidentifier") + .HasColumnName("Version"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + 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.Core.Scripting.ScriptLogRecord", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Entries") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.Property("TotalEntries") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Timestamp"); + + b.HasIndex("AppId", "Timestamp"); + + b.ToTable("ScriptLogs", (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") + .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") + .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"); + }); + + 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"); + }); + + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.Text.EFTextIndexUserInfoEntity", 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("UserInfoApiKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("UserInfoRole") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("UserInfoApiKey"); + + b.ToTable("UserInfos"); + }); + + 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") + .HasColumnType("uniqueidentifier"); + + b.Property("EventStream") + .IsRequired() + .HasMaxLength(750) + .HasColumnType("nvarchar(750)"); + + b.Property("EventStreamOffset") + .HasColumnType("bigint"); + + b.PrimitiveCollection("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") + .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/20260921120852_AddScriptLogs.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260921120852_AddScriptLogs.cs new file mode 100644 index 0000000000..c9e06384fa --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260921120852_AddScriptLogs.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Squidex.Providers.SqlServer.App.Migrations +{ + /// + public partial class AddScriptLogs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ScriptLogs", + columns: table => new + { + Id = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + AppId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + Name = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + Timestamp = table.Column(type: "datetimeoffset", nullable: false), + Entries = table.Column(type: "nvarchar(max)", nullable: false), + TotalEntries = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ScriptLogs", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ScriptLogs_AppId_Timestamp", + table: "ScriptLogs", + columns: new[] { "AppId", "Timestamp" }); + + migrationBuilder.CreateIndex( + name: "IX_ScriptLogs_Timestamp", + table: "ScriptLogs", + column: "Timestamp"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ScriptLogs"); + } + } +} 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 feccd05207..63eab56a7a 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 @@ -369,6 +369,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AssetKeyValueStore_TusMetadata", (string)null); }); + modelBuilder.Entity("Squidex.Domain.Apps.Core.Scripting.ScriptLogRecord", b => + { + b.Property("Id") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Entries") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.Property("TotalEntries") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Timestamp"); + + b.HasIndex("AppId", "Timestamp"); + + b.ToTable("ScriptLogs", (string)null); + }); + modelBuilder.Entity("Squidex.Domain.Apps.Entities.Apps.EFAppEntity", b => { b.Property("DocumentId") diff --git a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs index b550a36446..a738c596f8 100644 --- a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs +++ b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs @@ -37,6 +37,8 @@ using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Repositories; +using Squidex.Domain.Apps.Entities.Scripting; +using Squidex.Domain.Apps.Entities.Scripting.Repositories; using Squidex.Domain.Apps.Entities.Teams; using Squidex.Domain.Apps.Entities.Teams.Repositories; using Squidex.Domain.Users; @@ -237,6 +239,9 @@ private static void AddSquidexEntityFramework(this IS services.AddSingletonAs>() .As().As(); + services.AddSingletonAs>() + .As(); + services.AddSingletonAs>() .As(); diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Scripting/MongoScriptLogClassMap.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Scripting/MongoScriptLogClassMap.cs new file mode 100644 index 0000000000..afea4f207f --- /dev/null +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Scripting/MongoScriptLogClassMap.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson.Serialization; +using Squidex.Domain.Apps.Core.Scripting; + +namespace Squidex.Domain.Apps.Entities.Scripting; + +internal static class MongoScriptLogClassMap +{ + public static void RegisterClassMap() + { + BsonClassMap.TryRegisterClassMap(cm => + { + cm.MapProperty(x => x.Timestamp) + .SetElementName("t"); + + cm.MapProperty(x => x.Level) + .SetElementName("l"); + + cm.MapProperty(x => x.Message) + .SetElementName("m"); + + cm.MapCreator(x => new ScriptLogEntry(x.Timestamp, x.Level, x.Message)); + }); + } +} diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Scripting/MongoScriptLogRepository.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Scripting/MongoScriptLogRepository.cs new file mode 100644 index 0000000000..4f3062f7aa --- /dev/null +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Scripting/MongoScriptLogRepository.cs @@ -0,0 +1,109 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.RegularExpressions; +using MongoDB.Bson; +using MongoDB.Driver; +using NodaTime; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Scripting.Repositories; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Scripting; + +public sealed class MongoScriptLogRepository(IMongoDatabase database) : MongoRepositoryBase(database), IScriptLogRepository +{ + protected override string CollectionName() + { + return "ScriptLogs"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync( + [ + new CreateIndexModel( + Index + .Ascending(x => x.AppId) + .Descending(x => x.Timestamp)), + new CreateIndexModel( + Index + .Ascending(x => x.Timestamp)), + ], ct); + } + + public Task InsertManyAsync(IEnumerable records, + CancellationToken ct = default) + { + Guard.NotNull(records); + + var entities = records.ToList(); + + if (entities.Count == 0) + { + return Task.CompletedTask; + } + + return Collection.InsertManyAsync(entities, InsertUnordered, ct); + } + + public async Task TrimAsync(DomainId appId, int maxCount, + CancellationToken ct = default) + { + // Find the newest record that exceeds the limit. Everything that is not newer is deleted. + var boundary = + await Collection.Find(x => x.AppId == appId) + .SortByDescending(x => x.Timestamp) + .Skip(maxCount) + .Limit(1) + .Project(x => x.Timestamp) + .ToListAsync(ct); + + if (boundary.Count == 0) + { + return; + } + + var timestamp = boundary[0]; + + await Collection.DeleteManyAsync(x => x.AppId == appId && x.Timestamp <= timestamp, ct); + } + + public Task DeleteAsync(DomainId appId, + CancellationToken ct = default) + { + return Collection.DeleteManyAsync(x => x.AppId == appId, ct); + } + + public Task DeleteOlderThanAsync(Instant timestamp, + CancellationToken ct = default) + { + return Collection.DeleteManyAsync(x => x.Timestamp < timestamp, ct); + } + + public async Task> QueryAsync(DomainId appId, string? name, int skip, int take, + CancellationToken ct = default) + { + var filter = Filter.Eq(x => x.AppId, appId); + + if (!string.IsNullOrWhiteSpace(name)) + { + // The name is a path, therefore the filter matches all logs in the same group. + filter &= Filter.Regex(x => x.Name, new BsonRegularExpression($"^{Regex.Escape(name)}")); + } + + var result = + await Collection.Find(filter) + .SortByDescending(x => x.Timestamp) + .Skip(skip) + .Limit(take) + .ToListAsync(ct); + + return result; + } +} diff --git a/backend/src/Squidex.Data.MongoDb/MongoClientFactory.cs b/backend/src/Squidex.Data.MongoDb/MongoClientFactory.cs index 586f8f3237..2fbab841f9 100644 --- a/backend/src/Squidex.Data.MongoDb/MongoClientFactory.cs +++ b/backend/src/Squidex.Data.MongoDb/MongoClientFactory.cs @@ -16,6 +16,7 @@ using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.MongoDb; +using Squidex.Domain.Apps.Entities.Scripting; using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; @@ -48,6 +49,7 @@ public static void SetupSerializer(JsonSerializerOptions jsonSerializerOptions, MongoAssetFolderEntity.RegisterClassMap(); MongoContentEntity.RegisterClassMap(); MongoHistoryClassMap.RegisterClassMap(); + MongoScriptLogClassMap.RegisterClassMap(); MongoIdentityClassMap.RegisterClassMap(); MongoTextStateClassMap.RegisterClassMap(); diff --git a/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs b/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs index 335c436ec5..6b5da55e54 100644 --- a/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs +++ b/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs @@ -41,6 +41,8 @@ using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Repositories; +using Squidex.Domain.Apps.Entities.Scripting; +using Squidex.Domain.Apps.Entities.Scripting.Repositories; using Squidex.Domain.Apps.Entities.Teams; using Squidex.Domain.Apps.Entities.Teams.Repositories; using Squidex.Domain.Users; @@ -156,6 +158,9 @@ public static void AddSquidexMongoStore(this IServiceCollection services, IConfi services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As>(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs index 3450e900d4..57e930fffd 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs @@ -15,7 +15,7 @@ public sealed record AppClient(string Name, string Secret) { public string Name { get; init; } = Guard.NotNullOrEmpty(Name); - public string Secret { get; } = Guard.NotNullOrEmpty(Secret); + public string Secret { get; init; } = Guard.NotNullOrEmpty(Secret); public long ApiCallsLimit { get; init; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs index cdd874e22a..63ca5ad7d0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs @@ -56,6 +56,25 @@ public AppClients Add(string id, string secret, string? role = null) return new AppClients(updated); } + [Pure] + public AppClients RegenerateSecret(string id, string secret) + { + Guard.NotNullOrEmpty(id); + Guard.NotNullOrEmpty(secret); + + if (!TryGetValue(id, out var client)) + { + return this; + } + + if (!this.TrySet(id, client with { Secret = secret }, out var updated)) + { + return this; + } + + return new AppClients(updated); + } + [Pure] public AppClients Update(string id, string? name = null, string? role = null, long? apiCallsLimit = null, diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs index 379d5f4ffb..198b36d5dc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs @@ -53,6 +53,7 @@ public sealed class Roles WithoutPrefix(PermissionIds.AppRolesRead), WithoutPrefix(PermissionIds.AppRules), WithoutPrefix(PermissionIds.AppSchemas), + WithoutPrefix(PermissionIds.AppScriptLogs), WithoutPrefix(PermissionIds.AppWorkflows)), JsonValue.Object()), }; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs index a58160614a..1af6cd9533 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs @@ -19,57 +19,8 @@ public sealed class EventJintExtension(IUrlGenerator urlGenerator) : IJintExtens { private delegate JsValue EventDelegate(); - private sealed class FlowConsoleWrapper - { - public static readonly FlowConsoleWrapper Instance = new FlowConsoleWrapper(); - -#pragma warning disable CA1822 // Mark members as static - private void LogCore(string? message, string prefix) -#pragma warning restore CA1822 // Mark members as static - { - if (string.IsNullOrWhiteSpace(message)) - { - return; - } - - if (!string.IsNullOrWhiteSpace(prefix)) - { - message = $"{prefix}: {message}"; - } - - FlowConsole.Out(message); - } - - public void Log(string message) - { - LogCore(message, string.Empty); - } - - public void Info(string message) - { - LogCore(message, "INFO"); - } - - public void Warn(string message) - { - LogCore(message, "WARN"); - } - - public void Error(string message) - { - LogCore(message, "ERROR"); - } - - public void Debug(string message) - { - LogCore(message, "DEBUG"); - } - } - public void Extend(Engine engine) { - engine.SetValue("console", FlowConsoleWrapper.Instance); - engine.SetValue("contentAction", new EventDelegate(() => { if (engine.TryGetVar("event", out var contentEvent)) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs index 5685e196be..9a8e0bb626 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs @@ -75,6 +75,18 @@ internal static string ScriptingComplete { } } + internal static string ScriptingConsole { + get { + return ResourceManager.GetString("ScriptingConsole", resourceCulture); + } + } + + internal static string ScriptingConsoleLog { + get { + return ResourceManager.GetString("ScriptingConsoleLog", resourceCulture); + } + } + internal static string ScriptingContentAction { get { return ResourceManager.GetString("ScriptingContentAction", resourceCulture); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx b/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx index 92545f9660..ec52f0bc4f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx @@ -132,6 +132,12 @@ Completes the script when an async method is used. + + Writes values to the script log. + + + Writes the values to the script log. The log is shown in the script logs and in the rule execution log. + The status of the content. diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/ConsoleJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/ConsoleJintExtension.cs new file mode 100644 index 0000000000..8e6fdb7ad2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/ConsoleJintExtension.cs @@ -0,0 +1,140 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text; +using Jint; +using Jint.Native; +using Jint.Native.Json; +using Jint.Runtime.Interop; +using Microsoft.Extensions.Logging; +using Squidex.Domain.Apps.Core.Properties; +using Squidex.Flows; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Scripting.Extensions; + +public sealed class ConsoleJintExtension(ILogger log, IScriptLogStore? scriptLogStore = null) : IJintExtension, IScriptDescriptor +{ + private static readonly string[] Levels = ["log", "info", "warn", "error", "debug"]; + + public void Extend(Engine engine) + { + var console = new JsObject(engine); + + foreach (var level in Levels) + { + console.Set(level, new ClrFunction(engine, level, (_, args) => + { + Log(engine, level, args); + return JsValue.Undefined; + })); + } + + engine.SetValue("console", console); + } + + private void Log(Engine engine, string level, JsValue[] args) + { + var context = engine.GetContext(); + + var message = Format(engine, args); + + // Rules capture the output per flow step, the output is ignored outside of flows. + if (!string.IsNullOrWhiteSpace(message)) + { + FlowConsole.Out(level == "log" ? message : $"{level.ToUpperInvariant()}: {message}"); + } + + var scriptLog = ScriptLog.Current; + + if (scriptLog != null) + { + // The log is written once when the scope is completed. + if (!scriptLog.HasEntries && scriptLog.Persist && scriptLogStore != null && context.TryGetValueIfExists("appId", out var appId)) + { + scriptLog.OnCompleted(completed => scriptLogStore.Log(appId, completed.Name, completed)); + } + + scriptLog.Add(level, message); + } + + if (log.IsEnabled(LogLevel.Debug)) + { + context.TryGetValueIfExists("appName", out var appName); + + log.LogDebug("Script {name} in app {appName} logged with console.{level}: {message}", + scriptLog?.Name, + appName, + level, + message); + } + } + + private static string Format(Engine engine, JsValue[] args) + { + var sb = new StringBuilder(); + + foreach (var arg in args) + { + if (sb.Length > 0) + { + sb.Append(' '); + } + + sb.Append(Format(engine, arg)); + + // Protect the memory, the log truncates the message anyway. + if (sb.Length > ScriptLog.MaxLength) + { + break; + } + } + + return sb.ToString(); + } + + private static string Format(Engine engine, JsValue value) + { + if (value.IsString() || value.IsUndefined() || value.IsNull() || value.IsNumber() || value.IsBoolean()) + { + return value.ToString(); + } + + try + { + var json = new JsonSerializer(engine).Serialize(value); + + return json.IsString() ? json.ToString() : value.ToString(); + } + catch + { + // Cyclic objects cannot be serialized, therefore we fall back to the default representation. + return value.ToString(); + } + } + + public void Describe(AddDescription describe, ScriptScope scope) + { + describe(JsonType.Object, "console", + Resources.ScriptingConsole); + + describe(JsonType.Function, "console.log(value)", + Resources.ScriptingConsoleLog); + + describe(JsonType.Function, "console.info(value)", + Resources.ScriptingConsoleLog); + + describe(JsonType.Function, "console.warn(value)", + Resources.ScriptingConsoleLog); + + describe(JsonType.Function, "console.error(value)", + Resources.ScriptingConsoleLog); + + describe(JsonType.Function, "console.debug(value)", + Resources.ScriptingConsoleLog); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptLogStore.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptLogStore.cs new file mode 100644 index 0000000000..81e13d72b1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptLogStore.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Scripting; + +public interface IScriptLogStore +{ + void Log(DomainId appId, string name, ScriptLog log); + + Task> QueryAsync(DomainId appId, string? name, int skip, int take, + CancellationToken ct = default); +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptLog.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptLog.cs new file mode 100644 index 0000000000..620cf71c7c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptLog.cs @@ -0,0 +1,147 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class ScriptLog : IDisposable +{ + public const int MaxEntries = 100; + public const int MaxLength = 1000; + private static readonly AsyncLocal CurrentLog = new AsyncLocal(); + private readonly List entries = []; + private readonly List> completedCallbacks = []; + private readonly ScriptLog? previous; + private int totalEntries; + + public string Name { get; } + + public bool Persist { get; } + + public static ScriptLog? Current + { + get => CurrentLog.Value; + } + + public bool HasEntries + { + get + { + lock (entries) + { + return entries.Count > 0; + } + } + } + + private ScriptLog(string name, bool persist, ScriptLog? previous) + { + Name = name; + Persist = persist; + + this.previous = previous; + } + + public static ScriptLog Begin(string name, bool persist = false) + { + Guard.NotNullOrEmpty(name); + + var log = new ScriptLog(name, persist, CurrentLog.Value); + + CurrentLog.Value = log; + return log; + } + + public void Dispose() + { + if (CurrentLog.Value == this) + { + CurrentLog.Value = previous; + } + + Action[] callbacks; + + lock (entries) + { + callbacks = completedCallbacks.ToArray(); + completedCallbacks.Clear(); + } + + foreach (var callback in callbacks) + { + callback(this); + } + } + + public void OnCompleted(Action callback) + { + lock (entries) + { + completedCallbacks.Add(callback); + } + } + + public void Add(string level, string message) + { + // Scripts can log in loops, therefore the log is limited to protect the memory. + if (message.Length > MaxLength) + { + message = $"{message[..MaxLength]}..."; + } + + lock (entries) + { + totalEntries++; + + if (entries.Count < MaxEntries) + { + entries.Add(new ScriptLogEntry(SystemClock.Instance.GetCurrentInstant(), level, message)); + } + } + } + + public IReadOnlyList ToLines() + { + lock (entries) + { + var lines = entries.Select(x => x.ToString()).ToList(); + + if (totalEntries > entries.Count) + { + lines.Add($"... {totalEntries - entries.Count} more entries omitted."); + } + + return lines; + } + } + + public (IReadOnlyList Entries, int TotalEntries) Snapshot() + { + lock (entries) + { + return (entries.ToList(), totalEntries); + } + } + + public static async Task CollectAsync(string name, Func> action) + { + using var log = Begin(name, true); + + return await action(); + } + + public static Task CollectAsync(string name, Func action) + { + return CollectAsync(name, async () => + { + await action(); + return true; + }); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptLogEntry.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptLogEntry.cs new file mode 100644 index 0000000000..40c4a23640 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptLogEntry.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + +using NodaTime; + +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed record ScriptLogEntry(Instant Timestamp, string Level, string Message) +{ + public override string ToString() + { + return $"console.{Level}: {Message}"; + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptLogRecord.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptLogRecord.cs new file mode 100644 index 0000000000..5e7c1a1fe1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptLogRecord.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class ScriptLogRecord +{ + public DomainId Id { get; set; } = DomainId.NewGuid(); + + public DomainId AppId { get; set; } + + public string Name { get; set; } + + public Instant Timestamp { get; set; } + + public List Entries { get; set; } = []; + + public int TotalEntries { get; set; } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs index 0b4d6b57a8..b6e546c59b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs @@ -534,7 +534,8 @@ private void PushPrefix(string name) } } - [GeneratedRegex("^(?!\\d)[\\w$]+$", RegexOptions.Compiled)] + // Functions of objects are described with their parameters, e.g. 'console.log(value)'. + [GeneratedRegex("^(?!\\d)[\\w$]+(?:\\(.*\\))?$", RegexOptions.Compiled)] private static partial Regex BuildPropertyRegex(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs index 78e2344de6..197606ee4e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -30,6 +30,9 @@ public AppHistoryEventsCreator(TypeRegistry typeRegistry) AddEventMessage( "history.apps.clientRevoked"); + AddEventMessage( + "history.apps.clientSecretRegenerated"); + AddEventMessage( "history.apps.clientUpdated"); @@ -107,6 +110,8 @@ public AppHistoryEventsCreator(TypeRegistry typeRegistry) return CreateClientsEvent(e, e.Id); case AppClientRevoked e: return CreateClientsEvent(e, e.Id); + case AppClientSecretRegenerated e: + return CreateClientsEvent(e, e.Id); case AppLanguageAdded e: return CreateLanguagesEvent(e, e.Language); case AppLanguageUpdated e: diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RegenerateClientSecret.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RegenerateClientSecret.cs new file mode 100644 index 0000000000..d231611f80 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RegenerateClientSecret.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class RegenerateClientSecret : AppCommand +{ + public string Id { get; set; } + + public string Secret { get; set; } + + public RegenerateClientSecret() + { + Secret = RandomHash.New(); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs index 0f644cc5e4..d02cb2d51d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs @@ -75,6 +75,10 @@ protected override App Apply(App snapshot, Envelope @event) newSnapshot = snapshot.UpdateClients(e, (e, c) => c.Update(e.Id, e.Name, e.Role, e.ApiCallsLimit, e.ApiTrafficLimit, e.AllowAnonymous)); break; + case AppClientSecretRegenerated e: + newSnapshot = snapshot.UpdateClients(e, (e, c) => c.RegenerateSecret(e.Id, e.Secret)); + break; + case AppClientRevoked e: newSnapshot = snapshot.UpdateClients(e, (e, c) => c.Revoke(e.Id)); break; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs index 3d3fd446eb..875ee88467 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs @@ -178,6 +178,16 @@ public override Task ExecuteAsync(IAggregateCommand command, return Snapshot; }, ct); + case RegenerateClientSecret regenerateClientSecret: + return ApplyReturn(regenerateClientSecret, c => + { + GuardAppClients.CanRegenerateSecret(c, Snapshot); + + RegenerateClientSecret(c); + + return Snapshot; + }, ct); + case RevokeClient revokeClient: return ApplyReturn(revokeClient, c => { @@ -419,6 +429,11 @@ private void AttachClient(AttachClient command) Raise(command, new AppClientAttached()); } + private void RegenerateClientSecret(RegenerateClientSecret command) + { + Raise(command, new AppClientSecretRegenerated()); + } + private void RevokeClient(RevokeClient command) { Raise(command, new AppClientRevoked()); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppClients.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppClients.cs index 205e1fc97f..58d9039e02 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppClients.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppClients.cs @@ -49,6 +49,21 @@ public static void CanRevoke(RevokeClient command, App app) }); } + public static void CanRegenerateSecret(RegenerateClientSecret command, App app) + { + Guard.NotNull(command); + + GetClientOrThrow(app.Clients, command.Id); + + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + e(Not.Defined("ClientId"), nameof(command.Id)); + } + }); + } + public static void CanUpdate(UpdateClient command, App app) { Guard.NotNull(command); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs index 517ed512a4..57cbbad4ab 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs @@ -70,7 +70,7 @@ public static async Task ExecuteCreateScriptAsync(this AssetOperation operation, Tags = create.Tags?.ToReadonlyList(), }; - await ExecuteScriptAsync(operation, script, vars, asset, ct); + await ExecuteScriptAsync(operation, "create", script, vars, asset, ct); } public static Task ExecuteUpdateScriptAsync(this AssetOperation operation, UpdateAsset update, @@ -100,7 +100,7 @@ public static Task ExecuteUpdateScriptAsync(this AssetOperation operation, Updat Operation = "Update", }; - return ExecuteScriptAsync(operation, script, vars, null, ct); + return ExecuteScriptAsync(operation, "update", script, vars, null, ct); } public static Task ExecuteAnnotateScriptAsync(this AssetOperation operation, AnnotateAsset annotate, @@ -128,7 +128,7 @@ public static Task ExecuteAnnotateScriptAsync(this AssetOperation operation, Ann Operation = "Annotate", }; - return ExecuteScriptAsync(operation, script, vars, null, ct); + return ExecuteScriptAsync(operation, "annotate", script, vars, null, ct); } public static async Task ExecuteMoveScriptAsync(this AssetOperation operation, MoveAsset move, @@ -154,7 +154,7 @@ public static async Task ExecuteMoveScriptAsync(this AssetOperation operation, M Operation = "Move", }; - await ExecuteScriptAsync(operation, script, vars, null, ct); + await ExecuteScriptAsync(operation, "move", script, vars, null, ct); } public static Task ExecuteDeleteScriptAsync(this AssetOperation operation, DeleteAsset delete, @@ -177,10 +177,10 @@ public static Task ExecuteDeleteScriptAsync(this AssetOperation operation, Delet Operation = "Delete", }; - return ExecuteScriptAsync(operation, script, vars, null, ct); + return ExecuteScriptAsync(operation, "delete", script, vars, null, ct); } - private static async Task ExecuteScriptAsync(AssetOperation operation, string script, AssetScriptVars vars, AssetEntityScriptVars? asset, + private static async Task ExecuteScriptAsync(AssetOperation operation, string name, string script, AssetScriptVars vars, AssetEntityScriptVars? asset, CancellationToken ct) { var snapshot = operation.Snapshot; @@ -210,7 +210,10 @@ private static async Task ExecuteScriptAsync(AssetOperation operation, string sc var scriptEngine = operation.Resolve(); - await scriptEngine.ExecuteAsync(vars, script, Options, ct); + await ScriptLog.CollectAsync($"assets/{name}", async () => + { + await scriptEngine.ExecuteAsync(vars, script, Options, ct); + }); } private static async Task GetPathAsync(AssetOperation operation, DomainId parentId, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs index 7406407009..36c8534334 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs @@ -41,7 +41,7 @@ public static Task ExecuteCreateScriptAsync(this ContentOperation o StatusOld = default, }); - return TransformAsync(operation, script, vars, ct); + return TransformAsync(operation, "create", script, vars, ct); } public static Task ExecuteUpdateScriptAsync(this ContentOperation operation, ContentData data, @@ -66,7 +66,7 @@ public static Task ExecuteUpdateScriptAsync(this ContentOperation o StatusOld = default, }); - return TransformAsync(operation, script, vars, ct); + return TransformAsync(operation, "update", script, vars, ct); } public static Task ExecuteChangeScriptAsync(this ContentOperation operation, Status status, StatusChange change, @@ -92,7 +92,7 @@ public static Task ExecuteChangeScriptAsync(this ContentOperation o Validate = Validate(operation, status), }); - return TransformAsync(operation, script, vars, ct); + return TransformAsync(operation, "change", script, vars, ct); } public static Task ExecuteDeleteScriptAsync(this ContentOperation operation, bool permanent, @@ -118,19 +118,25 @@ public static Task ExecuteDeleteScriptAsync(this ContentOperation operation, boo StatusOld = default, }); - return ExecuteAsync(operation, script, vars, ct); + return ExecuteAsync(operation, "delete", script, vars, ct); } - private static async Task TransformAsync(ContentOperation operation, string script, ContentScriptVars vars, + private static async Task TransformAsync(ContentOperation operation, string name, string script, ContentScriptVars vars, CancellationToken ct) { - return await operation.Resolve().TransformAsync(vars, script, Options, ct); + return await ScriptLog.CollectAsync($"contents/{operation.Schema.Name}/{name}", async () => + { + return await operation.Resolve().TransformAsync(vars, script, Options, ct); + }); } - private static async Task ExecuteAsync(ContentOperation operation, string script, ContentScriptVars vars, + private static async Task ExecuteAsync(ContentOperation operation, string name, string script, ContentScriptVars vars, CancellationToken ct) { - await operation.Resolve().ExecuteAsync(vars, script, Options, ct); + await ScriptLog.CollectAsync($"contents/{operation.Schema.Name}/{name}", async () => + { + await operation.Resolve().ExecuteAsync(vars, script, Options, ct); + }); } private static Action Validate(ContentOperation operation, Status status) diff --git a/backend/src/Squidex.Domain.Apps.Entities/LogMessages.cs b/backend/src/Squidex.Domain.Apps.Entities/LogMessages.cs index 44d7ed2a50..5359dff42b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/LogMessages.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/LogMessages.cs @@ -89,4 +89,13 @@ internal static partial class LogMessages [LoggerMessage(Level = LogLevel.Error, Message = "Failed to execute search from source {source} with query '{query}'.")] public static partial void LogFailedToExecuteSearch(ILogger logger, string source, string query, Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to write script logs in background.")] + public static partial void LogFailedToWriteScriptLogs(ILogger logger, Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to clean up script logs.")] + public static partial void LogFailedToCleanupScriptLogs(ILogger logger, Exception exception); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Dropped {count} script logs, because the pending queue is full.")] + public static partial void LogScriptLogsDropped(ILogger logger, int count); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Scripting/BackgroundScriptLogStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Scripting/BackgroundScriptLogStore.cs new file mode 100644 index 0000000000..4384d3b10a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Scripting/BackgroundScriptLogStore.cs @@ -0,0 +1,208 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NodaTime; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Scripting.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Timers; + +namespace Squidex.Domain.Apps.Entities.Scripting; + +public sealed class BackgroundScriptLogStore : DisposableObjectBase, IScriptLogStore, IDeleter +{ + private readonly IScriptLogRepository repository; + private readonly ILogger log; + private readonly CompletionTimer timer; + private readonly ScriptLogOptions options; + private readonly ConcurrentQueue jobs = new ConcurrentQueue(); + private Instant lastCleanup; + private int jobsCount; + private int jobsDropped; + private bool isUpdating; + + public IClock Clock { get; set; } = SystemClock.Instance; + + public bool HasPendingJobs => !jobs.IsEmpty || isUpdating; + + public BackgroundScriptLogStore(IOptions options, + IScriptLogRepository repository, ILogger log) + { + this.options = options.Value; + this.repository = repository; + this.timer = new CompletionTimer(options.Value.WriteIntervall, TrackAsync, options.Value.WriteIntervall); + this.log = log; + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + timer.StopAsync().Wait(); + } + } + + public void Next() + { + ThrowIfDisposed(); + + timer.SkipCurrentDelay(); + } + + Task IDeleter.DeleteAppAsync(App app, + CancellationToken ct) + { + return repository.DeleteAsync(app.Id, ct); + } + + public void Log(DomainId appId, string name, ScriptLog log) + { + Guard.NotNull(log); + Guard.NotNullOrEmpty(name); + + if (!options.Enabled || !log.HasEntries) + { + return; + } + + // The queue is only drained by a timer. If the repository is not available for a longer time + // the queue would grow until the process runs out of memory, so new entries are dropped. + if (Volatile.Read(ref jobsCount) >= options.MaxPendingItems) + { + Interlocked.Increment(ref jobsDropped); + return; + } + + var (entries, totalEntries) = log.Snapshot(); + + Interlocked.Increment(ref jobsCount); + + jobs.Enqueue(new ScriptLogRecord + { + AppId = appId, + Name = name, + Entries = entries.ToList(), + TotalEntries = totalEntries, + Timestamp = Clock.GetCurrentInstant(), + }); + } + + public Task> QueryAsync(DomainId appId, string? name, int skip, int take, + CancellationToken ct = default) + { + if (!options.Enabled) + { + return Task.FromResult>([]); + } + + // More items than the limit are never stored, therefore the query is also limited. + return repository.QueryAsync( + appId, + name, + Math.Max(0, skip), + Math.Clamp(take, 0, options.MaxItemsPerApp), + ct); + } + + private async Task TrackAsync( + CancellationToken ct) + { + if (!options.Enabled) + { + return; + } + + isUpdating = true; + try + { + await WriteAsync(ct); + await CleanupAsync(ct); + } + finally + { + isUpdating = false; + } + } + + private async Task WriteAsync( + CancellationToken ct) + { + if (jobs.IsEmpty) + { + return; + } + + try + { + // Report the entries that have been dropped since the last run, so that the gap is visible. + var dropped = Interlocked.Exchange(ref jobsDropped, 0); + + if (dropped > 0) + { + LogMessages.LogScriptLogsDropped(log, dropped); + } + + var batch = new List(options.BatchSize); + var apps = new HashSet(); + + while (jobs.TryDequeue(out var dequeued)) + { + Interlocked.Decrement(ref jobsCount); + + batch.Add(dequeued); + apps.Add(dequeued.AppId); + + if (batch.Count >= options.BatchSize) + { + await repository.InsertManyAsync(batch.ToList(), ct); + batch.Clear(); + } + } + + if (batch.Count > 0) + { + await repository.InsertManyAsync(batch, ct); + } + + // Only the apps that got new logs can exceed the limit. + foreach (var appId in apps) + { + await repository.TrimAsync(appId, options.MaxItemsPerApp, ct); + } + } + catch (Exception ex) + { + LogMessages.LogFailedToWriteScriptLogs(log, ex); + } + } + + private async Task CleanupAsync( + CancellationToken ct) + { + var now = Clock.GetCurrentInstant(); + + if (now - lastCleanup < Duration.FromTimeSpan(options.CleanupIntervall)) + { + return; + } + + lastCleanup = now; + try + { + // Apps that do not log anymore are not trimmed, therefore old logs are removed separately. + await repository.DeleteOlderThanAsync(now.Minus(Duration.FromDays(options.RetentionInDays)), ct); + } + catch (Exception ex) + { + LogMessages.LogFailedToCleanupScriptLogs(log, ex); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Scripting/Repositories/IScriptLogRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Scripting/Repositories/IScriptLogRepository.cs new file mode 100644 index 0000000000..aacb1d08a6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Scripting/Repositories/IScriptLogRepository.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Scripting.Repositories; + +public interface IScriptLogRepository +{ + Task InsertManyAsync(IEnumerable records, + CancellationToken ct = default); + + Task TrimAsync(DomainId appId, int maxCount, + CancellationToken ct = default); + + Task DeleteAsync(DomainId appId, + CancellationToken ct = default); + + Task DeleteOlderThanAsync(Instant timestamp, + CancellationToken ct = default); + + Task> QueryAsync(DomainId appId, string? name, int skip, int take, + CancellationToken ct = default); +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptLogOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptLogOptions.cs new file mode 100644 index 0000000000..ec086264e6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptLogOptions.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Scripting; + +public sealed class ScriptLogOptions +{ + public bool Enabled { get; set; } = true; + + public int MaxItemsPerApp { get; set; } = 100; + + public int RetentionInDays { get; set; } = 7; + + public int BatchSize { get; set; } = 500; + + public int WriteIntervall { get; set; } = 1000; + + public TimeSpan CleanupIntervall { get; set; } = TimeSpan.FromMinutes(10); + + public int MaxPendingItems { get; set; } = 10_000; +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientSecretRegenerated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientSecretRegenerated.cs new file mode 100644 index 0000000000..308894398d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientSecretRegenerated.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppClientSecretRegenerated))] +public sealed class AppClientSecretRegenerated : AppEvent +{ + public string Id { get; set; } + + public string Secret { get; set; } +} diff --git a/backend/src/Squidex.Shared/PermissionIds.cs b/backend/src/Squidex.Shared/PermissionIds.cs index 49b57cbe83..c341b724f0 100644 --- a/backend/src/Squidex.Shared/PermissionIds.cs +++ b/backend/src/Squidex.Shared/PermissionIds.cs @@ -182,6 +182,10 @@ public static class PermissionIds public const string AppAssetSScriptsRead = "squidex.apps.{app}.asset-scripts.read"; public const string AppAssetsScriptsUpdate = "squidex.apps.{app}.asset-scripts.update"; + // App Script Logs + public const string AppScriptLogs = "squidex.apps.{app}.script-logs"; + public const string AppScriptLogsRead = "squidex.apps.{app}.script-logs.read"; + // App Rules public const string AppRules = "squidex.apps.{app}.rules"; public const string AppRulesRead = "squidex.apps.{app}.rules.read"; diff --git a/backend/src/Squidex.Shared/Texts.de.resx b/backend/src/Squidex.Shared/Texts.de.resx index 781073a04e..c8f39aea03 100644 --- a/backend/src/Squidex.Shared/Texts.de.resx +++ b/backend/src/Squidex.Shared/Texts.de.resx @@ -736,6 +736,9 @@ hat Client {[Id]} widerrufen + + regenerated secret of client {[Id]} + hat Client {[Id]} aktualisiert diff --git a/backend/src/Squidex.Shared/Texts.fr.resx b/backend/src/Squidex.Shared/Texts.fr.resx index 60a13a758c..49d5f81074 100644 --- a/backend/src/Squidex.Shared/Texts.fr.resx +++ b/backend/src/Squidex.Shared/Texts.fr.resx @@ -736,6 +736,9 @@ client révoqué {[Id]} + + regenerated secret of client {[Id]} + client mis à jour {[Id]} diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index 974684eeff..6d6efe5970 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -736,6 +736,9 @@ Revocato client {[Id]} + + regenerated secret of client {[Id]} + Aggiornato client {[Id]} diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index d1b5a36e2b..175fb3dff8 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -736,6 +736,9 @@ ingetrokken klant {[Id]} + + regenerated secret of client {[Id]} + bijgewerkte client {[Id]} diff --git a/backend/src/Squidex.Shared/Texts.pt.resx b/backend/src/Squidex.Shared/Texts.pt.resx index 598e5f9d63..7f0084dcdf 100644 --- a/backend/src/Squidex.Shared/Texts.pt.resx +++ b/backend/src/Squidex.Shared/Texts.pt.resx @@ -736,6 +736,9 @@ cliente {[Id]} revogado + + regenerated secret of client {[Id]} + cliente {[Id]} actualizado diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index 6ae4457721..c60378d2b4 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -736,6 +736,9 @@ revoked client {[Id]} + + regenerated secret of client {[Id]} + updated client {[Id]} diff --git a/backend/src/Squidex.Shared/Texts.zh.resx b/backend/src/Squidex.Shared/Texts.zh.resx index 9412439084..b2b3c69875 100644 --- a/backend/src/Squidex.Shared/Texts.zh.resx +++ b/backend/src/Squidex.Shared/Texts.zh.resx @@ -736,6 +736,9 @@ 已撤销客户端 {[Id]} + + regenerated secret of client {[Id]} + 更新的客户端 {[Id]} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 323f41df2c..c66f772a2d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -100,6 +100,30 @@ public async Task PutClient(string app, string id, [FromBody] Upd return Ok(response); } + /// + /// Regenerates the secret of an app client. + /// + /// The name of the app. + /// The ID of the client that must be updated. + /// Client secret regenerated. + /// Client or app not found. + /// + /// The client ID stays the same, but applications that use the old secret cannot request new access tokens anymore. + /// + [HttpPut] + [Route("apps/{app}/clients/{id}/secret/")] + [ProducesResponseType(typeof(ClientsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppClientsUpdate)] + [ApiCosts(1)] + public async Task PutClientSecret(string app, string id) + { + var command = new RegenerateClientSecret { Id = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + /// /// Revoke an app client. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index 58bb138d1c..a35550f6fb 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -12,6 +12,7 @@ using Squidex.Areas.Api.Controllers.Plans; using Squidex.Areas.Api.Controllers.Rules; using Squidex.Areas.Api.Controllers.Schemas; +using Squidex.Areas.Api.Controllers.ScriptLogs; using Squidex.Domain.Apps.Core.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; @@ -187,6 +188,12 @@ private AppDto CreateLinks(App app, Resources resources, PermissionSet permissio resources.Url(x => nameof(x.GetJobs), values)); } + if (resources.IsAllowed(PermissionIds.AppScriptLogsRead, Name, additional: permissions)) + { + AddGetLink("script-logs", + resources.Url(x => nameof(x.GetScriptLogs), values)); + } + if (resources.IsAllowed(PermissionIds.AppClientsRead, Name, additional: permissions)) { AddGetLink("clients", diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs index 3dda1fa350..7b69030c48 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs @@ -61,6 +61,9 @@ public ClientDto CreateLinks(Resources resources) { AddPutLink("update", resources.Url(x => nameof(x.PutClient), values)); + + AddPutLink("secret", + resources.Url(x => nameof(x.PutClientSecret), values)); } if (resources.CanDeleteClient) diff --git a/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/Models/ScriptLogDto.cs b/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/Models/ScriptLogDto.cs new file mode 100644 index 0000000000..3a2f0bbcbb --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/Models/ScriptLogDto.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.ScriptLogs.Models; + +public sealed class ScriptLogDto +{ + /// + /// The ID of the log. + /// + public DomainId Id { get; set; } + + /// + /// The name of the script, for example 'contents/my-schema/create' or 'assets/annotate'. + /// + public string Name { get; set; } + + /// + /// The time when the script has been executed. + /// + public Instant Timestamp { get; set; } + + /// + /// The log entries. + /// + public ScriptLogEntryDto[] Entries { get; set; } + + /// + /// The total number of entries the script has logged. Can be greater than the number of stored entries, because only the first entries are kept. + /// + public int TotalEntries { get; set; } + + public static ScriptLogDto FromDomain(ScriptLogRecord record) + { + return SimpleMapper.Map(record, new ScriptLogDto + { + Entries = record.Entries.Select(ScriptLogEntryDto.FromDomain).ToArray(), + }); + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/Models/ScriptLogEntryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/Models/ScriptLogEntryDto.cs new file mode 100644 index 0000000000..832190d5ed --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/Models/ScriptLogEntryDto.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.ScriptLogs.Models; + +public sealed class ScriptLogEntryDto +{ + /// + /// The time when the entry has been logged. + /// + public Instant Timestamp { get; set; } + + /// + /// The log level, for example 'log', 'info', 'warn' or 'error'. + /// + public string Level { get; set; } + + /// + /// The logged message. + /// + public string Message { get; set; } + + public static ScriptLogEntryDto FromDomain(ScriptLogEntry entry) + { + return SimpleMapper.Map(entry, new ScriptLogEntryDto()); + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/Models/ScriptLogsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/Models/ScriptLogsDto.cs new file mode 100644 index 0000000000..3d0fb177ce --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/Models/ScriptLogsDto.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Scripting; + +namespace Squidex.Areas.Api.Controllers.ScriptLogs.Models; + +public sealed class ScriptLogsDto +{ + /// + /// The script logs, newest first. + /// + public ScriptLogDto[] Items { get; set; } + + public static ScriptLogsDto FromDomain(IReadOnlyList logs) + { + return new ScriptLogsDto { Items = logs.Select(ScriptLogDto.FromDomain).ToArray() }; + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/ScriptLogsController.cs b/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/ScriptLogsController.cs new file mode 100644 index 0000000000..be1013eb19 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/ScriptLogs/ScriptLogsController.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; +using Squidex.Areas.Api.Controllers.ScriptLogs.Models; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure.Commands; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.ScriptLogs; + +/// +/// Readonly API to get the console output of scripts. +/// +[ApiExplorerSettings(GroupName = nameof(ScriptLogs))] +public sealed class ScriptLogsController(ICommandBus commandBus, IScriptLogStore scriptLogStore) : ApiController(commandBus) +{ + /// + /// Get the script logs. + /// + /// The name of the app. + /// The optional name or name prefix of the script, for example 'contents/my-schema'. + /// The number of logs to skip. + /// The number of logs to return. + /// Script logs returned. + /// App not found. + /// + /// Returns the console output of content and asset scripts, newest first. Only the latest logs are stored. + /// + [HttpGet] + [Route("apps/{app}/script-logs/")] + [ProducesResponseType(typeof(ScriptLogsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppScriptLogsRead)] + [ApiCosts(0.1)] + public async Task GetScriptLogs(string app, [FromQuery] string? name = null, [FromQuery] int skip = 0, [FromQuery] int take = 20) + { + var logs = await scriptLogStore.QueryAsync(AppId, name, skip, take, HttpContext.RequestAborted); + + var response = ScriptLogsDto.FromDomain(logs); + + return Ok(response); + } +} diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index c256f7fe22..90e4ac525d 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -17,7 +17,9 @@ using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Templates; using Squidex.Domain.Apps.Core.Templates.Extensions; +using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents.Counter; +using Squidex.Domain.Apps.Entities.Scripting; using Squidex.Domain.Apps.Entities.Tags; using Squidex.Hosting.Ssrf; using Squidex.Infrastructure; @@ -43,6 +45,9 @@ public static void AddSquidexInfrastructure(this IServiceCollection services, IC services.Configure(config, "scripting"); + services.Configure(config, + "scripting:logs"); + services.Configure(config, "diagnostics"); @@ -59,6 +64,9 @@ public static void AddSquidexInfrastructure(this IServiceCollection services, IC services.AddSingletonAs() .AsOptional(); + services.AddSingletonAs() + .As().As(); + services.AddSingletonAs() .AsSelf(); @@ -77,6 +85,9 @@ public static void AddSquidexInfrastructure(this IServiceCollection services, IC services.AddSingletonAs() .As().As(); + services.AddSingletonAs() + .As().As(); + services.AddSingletonAs() .As().As(); diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 4bac0683fb..5994644db2 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -152,7 +152,18 @@ "timeoutScript": "00:00:00.200", // The timeout for the asynchronous promise of the script. - "timeoutPromise": "00:00:04" + "timeoutPromise": "00:00:04", + + "logs": { + // False to disable storing the console output of content and asset scripts. + "enabled": true, + + // The number of script logs that are kept per app. Older logs are deleted. + "maxItemsPerApp": 100, + + // The number of days script logs are kept. + "retentionInDays": 7 + } }, "languages": { diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Scripting/EFScriptLogRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Scripting/EFScriptLogRepositoryTests.cs new file mode 100644 index 0000000000..0289312dfb --- /dev/null +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Scripting/EFScriptLogRepositoryTests.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.EntityFrameworkCore; +using Squidex.Domain.Apps.Entities.Scripting; +using Squidex.Domain.Apps.Entities.Scripting.Repositories; +using Squidex.EntityFramework.TestHelpers; +using Squidex.Shared; + +namespace Squidex.EntityFramework.Domain.Scripting; + +public abstract class EFScriptLogRepositoryTests(ISqlFixture fixture) : ScriptLogRepositoryTests where TContext : DbContext +{ + protected override Task CreateSutAsync() + { + var sut = new EFScriptLogRepository(fixture.DbContextFactory); + + return Task.FromResult(sut); + } +} diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Scripting/MongoScriptLogRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Scripting/MongoScriptLogRepositoryTests.cs new file mode 100644 index 0000000000..c3d1c22022 --- /dev/null +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Scripting/MongoScriptLogRepositoryTests.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Scripting; +using Squidex.Domain.Apps.Entities.Scripting.Repositories; +using Squidex.MongoDb.TestHelpers; +using Squidex.Shared; + +namespace Squidex.MongoDb.Domain.Scripting; + +[Trait("Category", "TestContainer")] +[Collection(MongoFixtureCollection.Name)] +public class MongoScriptLogRepositoryTests(MongoFixture fixture) : ScriptLogRepositoryTests +{ + protected override async Task CreateSutAsync() + { + var sut = new MongoScriptLogRepository(fixture.Database); + + await sut.InitializeAsync(default); + return sut; + } +} diff --git a/backend/tests/Squidex.Data.Tests/Shared/ScriptLogRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/Shared/ScriptLogRepositoryTests.cs new file mode 100644 index 0000000000..b2c9bb2b05 --- /dev/null +++ b/backend/tests/Squidex.Data.Tests/Shared/ScriptLogRepositoryTests.cs @@ -0,0 +1,167 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Scripting; +using Squidex.Domain.Apps.Entities.Scripting.Repositories; +using Squidex.Infrastructure; + +namespace Squidex.Shared; + +public abstract class ScriptLogRepositoryTests +{ + private readonly Instant now = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); + + protected abstract Task CreateSutAsync(); + + [Fact] + public async Task Should_insert_and_query_logs() + { + var sut = await CreateSutAsync(); + + var appId = DomainId.NewGuid(); + + var record = CreateRecord(appId, "contents/my-schema/create", 0); + + await sut.InsertManyAsync([record]); + + var actual = await sut.QueryAsync(appId, null, 0, 10); + + actual.Should().BeEquivalentTo([record]); + } + + [Fact] + public async Task Should_query_newest_first() + { + var sut = await CreateSutAsync(); + + var appId = DomainId.NewGuid(); + + await sut.InsertManyAsync( + [ + CreateRecord(appId, "assets/create", 1), + CreateRecord(appId, "assets/create", 3), + CreateRecord(appId, "assets/create", 2), + ]); + + var actual = await sut.QueryAsync(appId, null, 1, 10); + + Assert.Equal([now.Plus(Duration.FromSeconds(2)), now.Plus(Duration.FromSeconds(1))], actual.Select(x => x.Timestamp)); + } + + [Fact] + public async Task Should_query_by_name_prefix() + { + var sut = await CreateSutAsync(); + + var appId = DomainId.NewGuid(); + + await sut.InsertManyAsync( + [ + CreateRecord(appId, "contents/schema1/create", 1), + CreateRecord(appId, "contents/schema1/update", 2), + CreateRecord(appId, "contents/schema2/create", 3), + CreateRecord(appId, "assets/create", 4), + ]); + + var actual = await sut.QueryAsync(appId, "contents/schema1", 0, 10); + + Assert.Equal(["contents/schema1/update", "contents/schema1/create"], actual.Select(x => x.Name)); + } + + [Fact] + public async Task Should_trim_to_max_count() + { + var sut = await CreateSutAsync(); + + var appId = DomainId.NewGuid(); + var appIdOther = DomainId.NewGuid(); + + await sut.InsertManyAsync(Enumerable.Range(0, 10).Select(i => CreateRecord(appId, "assets/create", i))); + await sut.InsertManyAsync(Enumerable.Range(0, 10).Select(i => CreateRecord(appIdOther, "assets/create", i))); + + await sut.TrimAsync(appId, 3); + + var actual = await sut.QueryAsync(appId, null, 0, 100); + + Assert.Equal( + [ + now.Plus(Duration.FromSeconds(9)), + now.Plus(Duration.FromSeconds(8)), + now.Plus(Duration.FromSeconds(7)), + ], actual.Select(x => x.Timestamp)); + + var other = await sut.QueryAsync(appIdOther, null, 0, 100); + + Assert.Equal(10, other.Count); + } + + [Fact] + public async Task Should_not_trim_if_below_max_count() + { + var sut = await CreateSutAsync(); + + var appId = DomainId.NewGuid(); + + await sut.InsertManyAsync(Enumerable.Range(0, 3).Select(i => CreateRecord(appId, "assets/create", i))); + + await sut.TrimAsync(appId, 3); + + var actual = await sut.QueryAsync(appId, null, 0, 100); + + Assert.Equal(3, actual.Count); + } + + [Fact] + public async Task Should_delete_old_logs() + { + var sut = await CreateSutAsync(); + + var appId = DomainId.NewGuid(); + + await sut.InsertManyAsync(Enumerable.Range(0, 5).Select(i => CreateRecord(appId, "assets/create", i))); + + await sut.DeleteOlderThanAsync(now.Plus(Duration.FromSeconds(3))); + + var actual = await sut.QueryAsync(appId, null, 0, 100); + + Assert.Equal(2, actual.Count); + } + + [Fact] + public async Task Should_delete_logs_of_app() + { + var sut = await CreateSutAsync(); + + var appId = DomainId.NewGuid(); + + await sut.InsertManyAsync(Enumerable.Range(0, 5).Select(i => CreateRecord(appId, "assets/create", i))); + + await sut.DeleteAsync(appId); + + var actual = await sut.QueryAsync(appId, null, 0, 100); + + Assert.Empty(actual); + } + + private ScriptLogRecord CreateRecord(DomainId appId, string name, int seconds) + { + return new ScriptLogRecord + { + AppId = appId, + Name = name, + Entries = + [ + new ScriptLogEntry(now, "log", "Hello"), + new ScriptLogEntry(now.Plus(Duration.FromMilliseconds(5)), "warn", "World"), + ], + TotalEntries = 5, + Timestamp = now.Plus(Duration.FromSeconds(seconds)), + }; + } +} diff --git a/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj b/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj index d0c5c0e655..bcc57ee1c6 100644 --- a/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj +++ b/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj @@ -37,10 +37,10 @@ - - - - + + + + all diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs index 66c404ff39..1b7236c506 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs @@ -87,6 +87,22 @@ public void Should_return_same_clients_if_client_to_update_not_found() Assert.Same(clients_0, clients_1); } + [Fact] + public void Should_regenerate_secret() + { + var clients_1 = clients_0.RegenerateSecret("1", "new-secret"); + + Assert.Equal(new AppClient("1", "new-secret"), clients_1["1"]); + } + + [Fact] + public void Should_return_same_clients_if_client_to_regenerate_secret_not_found() + { + var clients_1 = clients_0.RegenerateSecret("2", "new-secret"); + + Assert.Same(clients_0, clients_1); + } + [Fact] public void Should_revoke_client() { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index a474b2caff..be7ee568a3 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs @@ -147,7 +147,7 @@ public void Should_check_for_non_default_role() Assert.False(Roles.IsDefault(firstRole)); } - [InlineData("Developer", 6)] + [InlineData("Developer", 7)] [InlineData("Editor", 4)] [InlineData("Reader", 2)] [InlineData("Owner", 1)] @@ -169,7 +169,7 @@ public void Should_get_default_roles(string name, int permissionCount) Assert.Equal(permissionCount, actual!.Permissions.Count); } - [InlineData("Developer", 15)] + [InlineData("Developer", 16)] [InlineData("Editor", 13)] [InlineData("Reader", 12)] [InlineData("Owner", 1)] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ConsoleJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ConsoleJintExtensionTests.cs new file mode 100644 index 0000000000..5875e536a8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ConsoleJintExtensionTests.cs @@ -0,0 +1,306 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.HandleRules.Extensions; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.Scripting.Extensions; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Flows; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Core.Operations.Scripting; + +public class ConsoleJintExtensionTests : IClassFixture +{ + private readonly IScriptLogStore scriptLogStore = A.Fake(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly IScriptEngine sut; + + public ConsoleJintExtensionTests() + { + var extensions = new IJintExtension[] + { + new ConsoleJintExtension(NullLogger.Instance, scriptLogStore), + }; + + sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions + { + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10), + }), + extensions); + } + + [Fact] + public void Should_collect_log_entries() + { + const string script = @" + console.log('Hello', 42, true, null, undefined, { a: 1 }, [1, 2]); + console.info('Info'); + console.warn('Warn'); + console.error('Error'); + "; + + using var log = ScriptLog.Begin("my-script"); + + sut.Execute(new ScriptVars(), script); + + Assert.Equal( + [ + "console.log: Hello 42 true null undefined {\"a\":1} [1,2]", + "console.info: Info", + "console.warn: Warn", + "console.error: Error", + ], log.ToLines()); + } + + [Fact] + public async Task Should_collect_log_entries_from_async_script() + { + var vars = new ScriptVars + { + ["value"] = "Hello", + }; + + const string script = @" + console.log(value); + "; + + using var log = ScriptLog.Begin("my-script"); + + await sut.ExecuteAsync(vars, script); + + Assert.Equal(["console.log: Hello"], log.ToLines()); + } + + [Fact] + public void Should_not_fail_without_scope() + { + const string script = @" + console.log('Hello'); + 42; + "; + + var actual = sut.Execute(new ScriptVars(), script).AsNumber; + + Assert.Equal(42, actual); + } + + [Fact] + public void Should_limit_number_of_entries() + { + const string script = @" + for (var i = 0; i < 150; i++) { + console.log(i); + } + "; + + using var log = ScriptLog.Begin("my-script"); + + sut.Execute(new ScriptVars(), script); + + var lines = log.ToLines(); + + Assert.Equal(ScriptLog.MaxEntries + 1, lines.Count); + Assert.Equal("... 50 more entries omitted.", lines[^1]); + Assert.Equal(150, log.Snapshot().TotalEntries); + } + + [Fact] + public void Should_truncate_long_entries() + { + const string script = @" + console.log('a'.repeat(2000)); + "; + + using var log = ScriptLog.Begin("my-script"); + + sut.Execute(new ScriptVars(), script); + + var line = Assert.Single(log.ToLines()); + + Assert.Equal($"console.log: {new string('a', ScriptLog.MaxLength)}...", line); + } + + [Fact] + public void Should_restore_previous_scope() + { + using var outer = ScriptLog.Begin("my-script"); + + using (var inner = ScriptLog.Begin("my-script")) + { + Assert.Same(inner, ScriptLog.Current); + } + + Assert.Same(outer, ScriptLog.Current); + } + + [Fact] + public async Task Should_not_add_log_entries_to_validation_error() + { + const string script = @" + console.log('Hello'); + reject('Failed'); + "; + + var ex = await Assert.ThrowsAsync(() => ScriptLog.CollectAsync("my-script", async () => + { + await sut.ExecuteAsync(new ScriptVars(), script, new ScriptOptions { CanReject = true }); + })); + + Assert.Equal(["Failed"], ex.Errors.Select(x => x.Message)); + } + + [Fact] + public void Should_write_to_flow_console() + { + const string script = @" + console.log('Log'); + console.info('Info'); + console.warn('Warn'); + console.error('Error'); + console.debug('Debug'); + "; + + var lines = new List(); + + FlowConsole.Output = (message, _) => lines.Add(message); + try + { + sut.Execute(new ScriptVars(), script); + } + finally + { + FlowConsole.Output = null!; + } + + Assert.Equal(["Log", "INFO: Info", "WARN: Warn", "ERROR: Error", "DEBUG: Debug"], lines); + } + + [Fact] + public async Task Should_not_be_replaced_by_rule_extension() + { + IScriptEngine engine = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions + { + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10), + }), + [ + new ConsoleJintExtension(NullLogger.Instance, scriptLogStore), + new EventJintExtension(A.Fake()), + ]); + + var vars = new ScriptVars + { + ["appId"] = appId, + }; + + const string script = @" + console.log('Hello'); + "; + + await ScriptLog.CollectAsync("my-script", async () => + { + await engine.ExecuteAsync(vars, script); + }); + + A.CallTo(() => scriptLogStore.Log(appId, "my-script", A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Should_store_log_of_named_scope() + { + var vars = new ScriptVars + { + ["appId"] = appId, + }; + + const string script = @" + console.log('Hello'); + console.log('World'); + "; + + await ScriptLog.CollectAsync("my-script", async () => + { + await sut.ExecuteAsync(vars, script); + }); + + A.CallTo(() => scriptLogStore.Log(appId, "my-script", A.That.Matches(x => x.ToLines().Count == 2))) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Should_store_log_if_script_failed() + { + var vars = new ScriptVars + { + ["appId"] = appId, + }; + + const string script = @" + console.log('Hello'); + reject('Failed'); + "; + + await Assert.ThrowsAsync(() => ScriptLog.CollectAsync("my-script", async () => + { + await sut.ExecuteAsync(vars, script, new ScriptOptions { CanReject = true }); + })); + + A.CallTo(() => scriptLogStore.Log(appId, "my-script", A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Should_not_store_log_without_entries() + { + var vars = new ScriptVars + { + ["appId"] = appId, + }; + + const string script = @" + 42; + "; + + await ScriptLog.CollectAsync("my-script", async () => + { + await sut.ExecuteAsync(vars, script); + }); + + A.CallTo(() => scriptLogStore.Log(A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public void Should_not_store_log_if_not_persisted() + { + var vars = new ScriptVars + { + ["appId"] = appId, + }; + + const string script = @" + console.log('Hello'); + "; + + using (ScriptLog.Begin("my-script")) + { + sut.Execute(vars, script); + } + + A.CallTo(() => scriptLogStore.Log(A._, A._, A._)) + .MustNotHaveHappened(); + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs index e911ac8faa..253c44c762 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs @@ -5,10 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.Extensions.Logging.Abstractions; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.GenerateFilters; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.Scripting.Extensions; using Squidex.Infrastructure.Queries; namespace Squidex.Domain.Apps.Core.Operations.Scripting; @@ -355,6 +357,20 @@ public void Should_describe_dynamic_usage_trigger() AssertUsageTrigger(actual); } + [Fact] + public void Should_describe_functions_of_objects_with_property_syntax() + { + var completer = new ScriptingCompleter([new ConsoleJintExtension(NullLogger.Instance)]); + + var actual = completer.ContentScript(dataSchema).Select(x => x.Path).ToArray(); + + Assert.Contains("console", actual); + Assert.Contains("console.log(value)", actual); + Assert.Contains("console.info(value)", actual); + Assert.Contains("console.warn(value)", actual); + Assert.Contains("console.error(value)", actual); + } + private static void AssertUsageTrigger(IReadOnlyList actual) { AssertCompletion(actual, diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs index b6ada10672..1af8b9a8dc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs @@ -374,6 +374,21 @@ public async Task UpdateClient_should_create_events_and_update_client() await VerifySutAsync(actual); } + [Fact] + public async Task RegenerateClientSecret_should_create_events_and_update_secret() + { + var command = new RegenerateClientSecret { Id = clientId }; + + await ExecuteCreateAsync(); + await ExecuteAttachClientAsync(); + + var actual = await PublishAsync(sut, command); + + await VerifySutAsync(actual); + + Assert.Equal(command.Secret, sut.Snapshot.Clients[clientId].Secret); + } + [Fact] public async Task RevokeClient_should_create_events_and_remove_client() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs index e855a135a2..6645f587da 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs @@ -82,6 +82,36 @@ public void CanRevoke_should_not_throw_exception_if_client_is_found() GuardAppClients.CanRevoke(command, App); } + [Fact] + public void CanRegenerateSecret_should_throw_execption_if_client_id_is_null() + { + var command = new RegenerateClientSecret(); + + ValidationAssert.Throws(() => GuardAppClients.CanRegenerateSecret(command, App), + new ValidationError("Client ID is required.", "Id")); + } + + [Fact] + public void CanRegenerateSecret_should_throw_exception_if_client_is_not_found() + { + var command = new RegenerateClientSecret { Id = "ios" }; + + Assert.Throws(() => GuardAppClients.CanRegenerateSecret(command, App)); + } + + [Fact] + public void CanRegenerateSecret_should_not_throw_exception_if_client_is_found() + { + var command = new RegenerateClientSecret { Id = "ios" }; + + App = App with + { + Clients = AppClients.Empty.Add("ios", "secret"), + }; + + GuardAppClients.CanRegenerateSecret(command, App); + } + [Fact] public void CanUpdate_should_throw_execption_if_client_id_is_null() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Scripting/BackgroundScriptLogStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Scripting/BackgroundScriptLogStoreTests.cs new file mode 100644 index 0000000000..c05527530d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Scripting/BackgroundScriptLogStoreTests.cs @@ -0,0 +1,201 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NodaTime; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Scripting.Repositories; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Scripting; + +public class BackgroundScriptLogStoreTests : GivenContext +{ + private readonly IScriptLogRepository repository = A.Fake(); + private readonly IClock clock = A.Fake(); + private readonly ScriptLogOptions options = new ScriptLogOptions(); + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); + private readonly BackgroundScriptLogStore sut; + + public BackgroundScriptLogStoreTests() + { + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(now); + + sut = new BackgroundScriptLogStore(Options.Create(options), repository, A.Fake>()) + { + Clock = clock, + }; + } + + [Fact] + public async Task Should_write_log_with_context() + { + var records = new List(); + + A.CallTo(() => repository.InsertManyAsync(A>._, A._)) + .Invokes(x => records.AddRange(x.GetArgument>(0)!)); + + sut.Log(AppId.Id, "contents/my-schema/create", CreateLog("Hello")); + + await WaitForCompletion(); + + var record = Assert.Single(records); + + Assert.Equal(AppId.Id, record.AppId); + Assert.Equal("contents/my-schema/create", record.Name); + Assert.Equal(now, record.Timestamp); + Assert.Equal([("log", "Hello")], record.Entries.Select(x => (x.Level, x.Message))); + Assert.Equal(1, record.TotalEntries); + } + + [Fact] + public async Task Should_trim_logs_of_written_apps() + { + var otherAppId = DomainId.NewGuid(); + + sut.Log(AppId.Id, "assets/create", CreateLog("Hello")); + sut.Log(AppId.Id, "assets/create", CreateLog("Hello")); + sut.Log(otherAppId, "assets/create", CreateLog("Hello")); + + await WaitForCompletion(); + + A.CallTo(() => repository.TrimAsync(AppId.Id, options.MaxItemsPerApp, A._)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => repository.TrimAsync(otherAppId, options.MaxItemsPerApp, A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Should_delete_old_logs() + { + sut.Log(AppId.Id, "assets/create", CreateLog("Hello")); + + await WaitForCompletion(); + + A.CallTo(() => repository.DeleteOlderThanAsync(now.Minus(Duration.FromDays(options.RetentionInDays)), A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Should_not_write_log_without_entries() + { + using var log = ScriptLog.Begin("my-script"); + + sut.Log(AppId.Id, "assets/create", log); + + await WaitForCompletion(); + + A.CallTo(() => repository.InsertManyAsync(A>._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_write_log_if_disabled() + { + options.Enabled = false; + + sut.Log(AppId.Id, "assets/create", CreateLog("Hello")); + + await WaitForCompletion(); + + A.CallTo(() => repository.InsertManyAsync(A>._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_drop_logs_if_queue_is_full() + { + options.MaxPendingItems = 2; + + var records = new List(); + + A.CallTo(() => repository.InsertManyAsync(A>._, A._)) + .Invokes(x => records.AddRange(x.GetArgument>(0)!)); + + for (var i = 0; i < 5; i++) + { + sut.Log(AppId.Id, "assets/create", CreateLog("Hello")); + } + + await WaitForCompletion(); + + Assert.Equal(2, records.Count); + } + + [Fact] + public async Task Should_query_logs_from_repository() + { + var records = new List { new ScriptLogRecord() }; + + A.CallTo(() => repository.QueryAsync(AppId.Id, "contents", 10, 20, CancellationToken)) + .Returns(records); + + var actual = await sut.QueryAsync(AppId.Id, "contents", 10, 20, CancellationToken); + + Assert.Same(records, actual); + } + + [Fact] + public async Task Should_limit_query_to_max_items() + { + await sut.QueryAsync(AppId.Id, null, -5, 1000, CancellationToken); + + A.CallTo(() => repository.QueryAsync(AppId.Id, null, 0, options.MaxItemsPerApp, CancellationToken)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_query_logs_if_disabled() + { + options.Enabled = false; + + var actual = await sut.QueryAsync(AppId.Id, null, 0, 20, CancellationToken); + + Assert.Empty(actual); + + A.CallTo(() => repository.QueryAsync(A._, A._, A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_delete_logs_if_app_is_deleted() + { + await ((IDeleter)sut).DeleteAppAsync(App, CancellationToken); + + A.CallTo(() => repository.DeleteAsync(AppId.Id, CancellationToken)) + .MustHaveHappened(); + } + + private static ScriptLog CreateLog(string message) + { + using var log = ScriptLog.Begin("my-script"); + + log.Add("log", message); + + return log; + } + + private async Task WaitForCompletion() + { + sut.Next(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + while (sut.HasPendingJobs) + { + cts.Token.ThrowIfCancellationRequested(); + + await Task.Delay(20, cts.Token); + } + + sut.Dispose(); + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index d3a7c1b1c3..d27e77d3bb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -38,7 +38,7 @@ - + diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddLanguage_should_create_events_and_add_language.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddLanguage_should_create_events_and_add_language.verified.txt index ac040795d7..43cdd8da38 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddLanguage_should_create_events_and_add_language.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddLanguage_should_create_events_and_add_language.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddRole_should_create_events_and_add_role.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddRole_should_create_events_and_add_role.verified.txt index a5d20fd2f7..dc8c4e5053 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddRole_should_create_events_and_add_role.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddRole_should_create_events_and_add_role.verified.txt @@ -82,6 +82,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddWorkflow_should_create_events_and_add_workflow.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddWorkflow_should_create_events_and_add_workflow.verified.txt index 7a7d5096ca..57ab507715 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddWorkflow_should_create_events_and_add_workflow.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddWorkflow_should_create_events_and_add_workflow.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_events_and_add_contributor.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_events_and_add_contributor.verified.txt index c3c4e65aec..a8622eab35 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_events_and_add_contributor.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_events_and_add_contributor.verified.txt @@ -72,6 +72,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_update_events_and_update_contributor.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_update_events_and_update_contributor.verified.txt index 8066bf2981..569bda08f9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_update_events_and_update_contributor.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_update_events_and_update_contributor.verified.txt @@ -72,6 +72,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AttachClient_should_create_events_and_add_client.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AttachClient_should_create_events_and_add_client.verified.txt index 74e8c18a3d..a1632a80b3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AttachClient_should_create_events_and_add_client.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AttachClient_should_create_events_and_add_client.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_create_events_and_update_plan.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_create_events_and_update_plan.verified.txt index 1426eefd7c..d962be566f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_create_events_and_update_plan.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_create_events_and_update_plan.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_reset_plan_for_free_plan.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_reset_plan_for_free_plan.verified.txt index 6089f817d5..3bb0569a21 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_reset_plan_for_free_plan.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_reset_plan_for_free_plan.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_create_events_and_update_plan.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_create_events_and_update_plan.verified.txt index 1426eefd7c..d962be566f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_create_events_and_update_plan.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_create_events_and_update_plan.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_call_billing_manager_for_callback.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_call_billing_manager_for_callback.verified.txt index 1426eefd7c..d962be566f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_call_billing_manager_for_callback.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_call_billing_manager_for_callback.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_make_update_for_redirect.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_make_update_for_redirect.verified.txt index 9301c32f81..136e43e73d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_make_update_for_redirect.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_make_update_for_redirect.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_reset_plan_for_free_plan.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_reset_plan_for_free_plan.verified.txt index 6089f817d5..3bb0569a21 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_reset_plan_for_free_plan.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_reset_plan_for_free_plan.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_create_events_and_set_intitial_state.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_create_events_and_set_intitial_state.verified.txt index e6b6b94720..19aa1c7fa1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_create_events_and_set_intitial_state.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_create_events_and_set_intitial_state.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_not_assign_client_as_contributor.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_not_assign_client_as_contributor.verified.txt index e0a1d371f0..d2d3025949 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_not_assign_client_as_contributor.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_not_assign_client_as_contributor.verified.txt @@ -68,6 +68,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteRole_should_create_events_and_delete_role.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteRole_should_create_events_and_delete_role.verified.txt index d6305ce14e..2ce1a6d4db 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteRole_should_create_events_and_delete_role.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteRole_should_create_events_and_delete_role.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteWorkflow_should_create_events_and_remove_workflow.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteWorkflow_should_create_events_and_remove_workflow.verified.txt index 37b40b78ce..b8ab55eebc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteWorkflow_should_create_events_and_remove_workflow.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteWorkflow_should_create_events_and_remove_workflow.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Delete_should_create_events_and_update_deleted_flag.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Delete_should_create_events_and_update_deleted_flag.verified.txt index 644c1917a7..5e0a07dd2d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Delete_should_create_events_and_update_deleted_flag.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Delete_should_create_events_and_update_deleted_flag.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Delete_should_create_events_with_permanent_flag.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Delete_should_create_events_with_permanent_flag.verified.txt index 959f48fcbf..310de6ca08 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Delete_should_create_events_with_permanent_flag.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Delete_should_create_events_with_permanent_flag.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RegenerateClientSecret_should_create_events_and_update_secret.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RegenerateClientSecret_should_create_events_and_update_secret.verified.txt new file mode 100644 index 0000000000..4121231e1a --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RegenerateClientSecret_should_create_events_and_update_secret.verified.txt @@ -0,0 +1,132 @@ +{ + sut: { + UniqueId: Guid_1, + Snapshot: { + Name: my-app, + Contributors: { + me: Owner + }, + Roles: { + All: [ + { + Name: Owner, + Permissions: [ + { + Id: * + } + ], + IsDefault: true + }, + { + Name: Reader, + Properties: { + ui.api.hide: true + }, + Permissions: [ + { + Id: assets.read + }, + { + Id: contents.*.read + } + ], + IsDefault: true + }, + { + Name: Editor, + Properties: { + ui.api.hide: true + }, + Permissions: [ + { + Id: assets + }, + { + Id: contents.* + }, + { + Id: roles.read + }, + { + Id: workflows.read + } + ], + IsDefault: true + }, + { + Name: Developer, + Permissions: [ + { + Id: assets + }, + { + Id: contents.* + }, + { + Id: roles.read + }, + { + Id: rules + }, + { + Id: schemas + }, + { + Id: script-logs + }, + { + Id: workflows + } + ], + IsDefault: true + } + ] + }, + Clients: { + client: { + Name: client, + AllowAnonymous: false, + Role: Editor + } + }, + Settings: { + HideScheduler: true, + HideDateTimeModeButton: false + }, + AssetScripts: {}, + Languages: { + Master: en, + AllKeys: [ + en + ], + Values: { + en: { + IsOptional: false + } + } + }, + IsDeleted: false, + Id: Guid_1, + CreatedBy: subject:me, + LastModifiedBy: subject:me, + Version: 4, + UniqueId: Guid_1 + }, + Version: 4, + ObjectState: Created + }, + events: [ + { + Headers: { + AggregateId: Guid_1, + EventId: Guid_2 + }, + Payload: { + Id: client, + AppId: Guid_1,my-app, + Actor: subject:me, + FromRule: false + } + } + ] +} \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveContributor_should_create_events_and_remove_contributor.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveContributor_should_create_events_and_remove_contributor.verified.txt index b565806104..a2c904d3e6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveContributor_should_create_events_and_remove_contributor.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveContributor_should_create_events_and_remove_contributor.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveImage_should_create_events_and_update_image.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveImage_should_create_events_and_update_image.verified.txt index 6089f817d5..3bb0569a21 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveImage_should_create_events_and_update_image.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveImage_should_create_events_and_update_image.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveLanguage_should_create_events_and_remove_language.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveLanguage_should_create_events_and_remove_language.verified.txt index 6e6ebcceb4..dc23b8f142 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveLanguage_should_create_events_and_remove_language.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveLanguage_should_create_events_and_remove_language.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RevokeClient_should_create_events_and_remove_client.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RevokeClient_should_create_events_and_remove_client.verified.txt index abba6bb5c8..9cc90a23e9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RevokeClient_should_create_events_and_remove_client.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RevokeClient_should_create_events_and_remove_client.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_from_team_should_create_events_and_set_team.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_from_team_should_create_events_and_set_team.verified.txt index 6089f817d5..3bb0569a21 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_from_team_should_create_events_and_set_team.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_from_team_should_create_events_and_set_team.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_should_create_events_and_set_team.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_should_create_events_and_set_team.verified.txt index 10238f91eb..3579053221 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_should_create_events_and_set_team.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_should_create_events_and_set_team.verified.txt @@ -72,6 +72,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateClient_should_create_events_and_update_client.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateClient_should_create_events_and_update_client.verified.txt index 275ae2e341..050ec53585 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateClient_should_create_events_and_update_client.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateClient_should_create_events_and_update_client.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateLanguage_should_create_events_and_update_language.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateLanguage_should_create_events_and_update_language.verified.txt index 4aa3570348..aeded89f57 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateLanguage_should_create_events_and_update_language.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateLanguage_should_create_events_and_update_language.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateRole_should_create_events_and_update_role.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateRole_should_create_events_and_update_role.verified.txt index 96894bbe45..9c585e7802 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateRole_should_create_events_and_update_role.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateRole_should_create_events_and_update_role.verified.txt @@ -92,6 +92,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateSettings_should_create_event_and_update_settings.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateSettings_should_create_event_and_update_settings.verified.txt index bf8386aaf7..dc9e6ae5c3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateSettings_should_create_event_and_update_settings.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateSettings_should_create_event_and_update_settings.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateWorkflow_should_create_events_and_update_workflow.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateWorkflow_should_create_events_and_update_workflow.verified.txt index 65a5ac74ce..739ff4fb66 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateWorkflow_should_create_events_and_update_workflow.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateWorkflow_should_create_events_and_update_workflow.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Update_should_create_events_and_update_label_and_description.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Update_should_create_events_and_update_label_and_description.verified.txt index 227c534beb..cfc82aa5be 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Update_should_create_events_and_update_label_and_description.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Update_should_create_events_and_update_label_and_description.verified.txt @@ -73,6 +73,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UploadImage_should_create_events_and_update_image.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UploadImage_should_create_events_and_update_image.verified.txt index 6d075d669b..1057901b3a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UploadImage_should_create_events_and_update_image.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UploadImage_should_create_events_and_update_image.verified.txt @@ -71,6 +71,9 @@ { Id: schemas }, + { + Id: script-logs + }, { Id: workflows } diff --git a/frontend/src/app/features/settings/pages/clients/client.component.html b/frontend/src/app/features/settings/pages/clients/client.component.html index 7a782cc74f..f69867af54 100644 --- a/frontend/src/app/features/settings/pages/clients/client.component.html +++ b/frontend/src/app/features/settings/pages/clients/client.component.html @@ -54,6 +54,18 @@ type="button"> + @if (client.canRegenerateSecret) { + + } diff --git a/frontend/src/app/features/settings/pages/clients/client.component.ts b/frontend/src/app/features/settings/pages/clients/client.component.ts index 105b448bcd..5dd25ebab6 100644 --- a/frontend/src/app/features/settings/pages/clients/client.component.ts +++ b/frontend/src/app/features/settings/pages/clients/client.component.ts @@ -56,6 +56,10 @@ export class ClientComponent { this.clientsState.revoke(this.client); } + public regenerateSecret() { + this.clientsState.regenerateSecret(this.client); + } + public updateRole(role: string) { const request = new UpdateClientDto({ role }); diff --git a/frontend/src/app/features/settings/pages/script-logs/script-log.component.html b/frontend/src/app/features/settings/pages/script-logs/script-log.component.html new file mode 100644 index 0000000000..aa031cf50d --- /dev/null +++ b/frontend/src/app/features/settings/pages/script-logs/script-log.component.html @@ -0,0 +1,36 @@ +
+
+
+
+

{{ scriptLog.name }}

+ +
+ {{ scriptLog.timestamp | sqxFromNow }} + {{ scriptLog.totalEntries }} +
+
+ +
+ +
+
+
+ + @if (isExpanded) { +
+
+ @for (line of lines; track $index) { +
+ {{ line.time }} [{{ line.level }}] {{ line.message }} +
+ } + + @if (omitted > 0) { +
{{ "scriptLogs.omitted" | sqxTranslate: { count: omitted } }}
+ } +
+
+ } +
diff --git a/frontend/src/app/features/settings/pages/script-logs/script-log.component.scss b/frontend/src/app/features/settings/pages/script-logs/script-log.component.scss new file mode 100644 index 0000000000..a82d2592dc --- /dev/null +++ b/frontend/src/app/features/settings/pages/script-logs/script-log.component.scss @@ -0,0 +1,68 @@ +@import 'mixins'; +@import 'vars'; + +$console-background: #1e1f22; +$console-text: #d4d4d4; +$console-muted: #7f848e; +$console-info: #4fc1ff; +$console-warn: #e5c07b; +$console-error: #f48771; + +h4 { + font-size: 1rem; + font-weight: 500; + margin: 0; +} + +.console { + background: $console-background; + border-bottom-left-radius: $border-radius; + border-bottom-right-radius: $border-radius; + color: $console-text; + font-family: monospace; + font-size: .85rem; + line-height: 1.5; + max-height: 30rem; + overflow-y: hidden; + overflow-y: auto; + padding: .75rem 1rem; + + &-line { + white-space: pre-wrap; + word-break: break-word; + } + + &-level { + display: inline-block; + margin-left: 1rem; + margin-right: 1rem; + min-width: 7ch; + } + + &-time, + &-debug { + color: $console-muted; + } + + &-info { + color: $console-info; + } + + &-warn { + color: $console-warn; + } + + &-error { + color: $console-error; + } + + &-omitted { + color: $console-muted; + font-style: italic; + font-weight: normal; + } +} + +.btn-expand.expanded::before { + bottom: -1.35rem; +} diff --git a/frontend/src/app/features/settings/pages/script-logs/script-log.component.ts b/frontend/src/app/features/settings/pages/script-logs/script-log.component.ts new file mode 100644 index 0000000000..b4d4b1f877 --- /dev/null +++ b/frontend/src/app/features/settings/pages/script-logs/script-log.component.ts @@ -0,0 +1,46 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { FromNowPipe, ScriptLogDto, TranslatePipe, TypedSimpleChanges } from '@app/shared'; + +@Component({ + selector: 'sqx-script-log', + styleUrls: ['./script-log.component.scss'], + templateUrl: './script-log.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + FromNowPipe, + TranslatePipe, + ], +}) +export class ScriptLogComponent { + @Input({ required: true }) + public scriptLog!: ScriptLogDto; + + public lines: ReadonlyArray<{ time: string; level: string; message: string }> = []; + + public omitted = 0; + + public isExpanded = false; + + public ngOnChanges(changes: TypedSimpleChanges) { + if (changes.scriptLog) { + this.lines = this.scriptLog.entries.map(x => ({ + time: x.timestamp?.toStringFormat('yyyy-MM-dd HH:mm:ss.SSS') ?? '', + level: x.level, + message: x.message, + })); + + this.omitted = Math.max(0, this.scriptLog.totalEntries - this.scriptLog.entries.length); + } + } + + public toggleExpanded() { + this.isExpanded = !this.isExpanded; + } +} diff --git a/frontend/src/app/features/settings/pages/script-logs/script-logs-page.component.html b/frontend/src/app/features/settings/pages/script-logs/script-logs-page.component.html new file mode 100644 index 0000000000..84868e2785 --- /dev/null +++ b/frontend/src/app/features/settings/pages/script-logs/script-logs-page.component.html @@ -0,0 +1,33 @@ + + + + +
+ +
+
+ + + {{ "scriptLogs.description" | sqxTranslate }} + + @if ((scriptLogsState.isLoaded | async) && (scriptLogsState.scriptLogs | async); as scriptLogs) { + @if (scriptLogs.length === 0) { +
{{ "scriptLogs.empty" | sqxTranslate }}
+ } + + @for (scriptLog of scriptLogs; track scriptLog.id) { + + } + } +
+
+
diff --git a/frontend/src/app/features/settings/pages/script-logs/script-logs-page.component.scss b/frontend/src/app/features/settings/pages/script-logs/script-logs-page.component.scss new file mode 100644 index 0000000000..6fbfef1194 --- /dev/null +++ b/frontend/src/app/features/settings/pages/script-logs/script-logs-page.component.scss @@ -0,0 +1,6 @@ +@import 'mixins'; +@import 'vars'; + +.form-control { + width: 20rem; +} diff --git a/frontend/src/app/features/settings/pages/script-logs/script-logs-page.component.ts b/frontend/src/app/features/settings/pages/script-logs/script-logs-page.component.ts new file mode 100644 index 0000000000..6ea60fdcb1 --- /dev/null +++ b/frontend/src/app/features/settings/pages/script-logs/script-logs-page.component.ts @@ -0,0 +1,47 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { AsyncPipe } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { FormAlertComponent, LayoutComponent, ListViewComponent, ScriptLogsState, ShortcutDirective, TitleComponent, TranslatePipe } from '@app/shared'; +import { ScriptLogComponent } from './script-log.component'; + +@Component({ + selector: 'sqx-script-logs-page', + styleUrls: ['./script-logs-page.component.scss'], + templateUrl: './script-logs-page.component.html', + imports: [ + AsyncPipe, + FormAlertComponent, + FormsModule, + LayoutComponent, + ListViewComponent, + ScriptLogComponent, + ShortcutDirective, + TitleComponent, + TranslatePipe, + ], +}) +export class ScriptLogsPageComponent implements OnInit { + constructor( + public readonly scriptLogsState: ScriptLogsState, + ) { + } + + public ngOnInit() { + this.scriptLogsState.load(); + } + + public reload() { + this.scriptLogsState.load(true); + } + + public filter(name: string) { + this.scriptLogsState.filter(name); + } +} diff --git a/frontend/src/app/features/settings/routes.ts b/frontend/src/app/features/settings/routes.ts index 7dfb6a2137..465d03bdbe 100644 --- a/frontend/src/app/features/settings/routes.ts +++ b/frontend/src/app/features/settings/routes.ts @@ -15,6 +15,7 @@ import { LanguagesPageComponent } from './pages/languages/languages-page.compone import { MorePageComponent } from './pages/more/more-page.component'; import { PlansPageComponent } from './pages/plans/plans-page.component'; import { RolesPageComponent } from './pages/roles/roles-page.component'; +import { ScriptLogsPageComponent } from './pages/script-logs/script-logs-page.component'; import { SettingsPageComponent } from './pages/settings/settings-page.component'; import { WorkflowsPageComponent } from './pages/workflows/workflows-page.component'; import { SettingsAreaComponent } from './settings-area.component'; @@ -221,6 +222,10 @@ export const SETTINGS_ROUTES: Routes = [ }, ], }, + { + path: 'script-logs', + component: ScriptLogsPageComponent, + }, ], }, ]; \ No newline at end of file diff --git a/frontend/src/app/features/settings/settings-menu.component.html b/frontend/src/app/features/settings/settings-menu.component.html index a487a6ad1e..c3a63bb127 100644 --- a/frontend/src/app/features/settings/settings-menu.component.html +++ b/frontend/src/app/features/settings/settings-menu.component.html @@ -59,6 +59,12 @@ } + @if (app.canReadScriptLogs) { + + } + @if (app.canReadPlans) {