From 92c2bc4fe8a3175bffc092c8823fc42521bb1647 Mon Sep 17 00:00:00 2001 From: Mika Aaron Herrmann <65243124+MH321Productions@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:52:32 +0200 Subject: [PATCH] Consumer API: Multiple active identity deletion processes can exist (#843) * feat: Use Semaphore (only temporary) * chore: Remove semaphore * fix: Use unique filtered index * feat: Add filtered index to SQL Server * feat: Catch Update Exception to throw a 400 response * chore: Fix formatting * fix: remove code added to Init migrations * chore: delete MigrationOperations * feat: add migration via EntityTypeConfiguration * chore: simplify ef core scripts by using DatabaseMigrator as startup assembly * chore: Handle update exception in repository * fix: correct casing of exception message recognition --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Timo Notheisen --- .../AdminApi/src/AdminApi/AdminApi.csproj | 4 - .../ConsumerApi/src/ConsumerApi.csproj | 49 +- .../DatabaseMigrator/DatabaseMigrator.csproj | 94 +- .../Extensions/DbUpdateExceptionExtensions.cs | 2 +- .../StartDeletionProcessAsOwner/Handler.cs | 12 +- ...neActiveDeletionProcessAllowedException.cs | 14 + .../Identities/IdentityDeletionProcess.cs | 6 + ...IndexOnActiveDeletionProcesses.Designer.cs | 925 +++++++++++++++++ ...AddUniqueIndexOnActiveDeletionProcesses.cs | 59 ++ .../DevicesDbContextModelSnapshot.cs | 8 +- ...IndexOnActiveDeletionProcesses.Designer.cs | 928 ++++++++++++++++++ ...AddUniqueIndexOnActiveDeletionProcesses.cs | 59 ++ .../DevicesDbContextModelSnapshot.cs | 8 +- ...yDeletionProcessEntityTypeConfiguration.cs | 3 + .../Repository/IdentitiesRepository.cs | 11 +- scripts/windows/efcore/add_migration.ps1 | 10 +- scripts/windows/efcore/remove_migration.ps1 | 11 +- .../windows/efcore/update_local_database.ps1 | 15 +- 18 files changed, 2112 insertions(+), 106 deletions(-) create mode 100644 Modules/Devices/src/Devices.Application/OnlyOneActiveDeletionProcessAllowedException.cs create mode 100644 Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240909071633_AddUniqueIndexOnActiveDeletionProcesses.Designer.cs create mode 100644 Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240909071633_AddUniqueIndexOnActiveDeletionProcesses.cs create mode 100644 Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240909071602_AddUniqueIndexOnActiveDeletionProcesses.Designer.cs create mode 100644 Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240909071602_AddUniqueIndexOnActiveDeletionProcesses.cs diff --git a/Applications/AdminApi/src/AdminApi/AdminApi.csproj b/Applications/AdminApi/src/AdminApi/AdminApi.csproj index 6e2dcbf540..383d8115d7 100644 --- a/Applications/AdminApi/src/AdminApi/AdminApi.csproj +++ b/Applications/AdminApi/src/AdminApi/AdminApi.csproj @@ -7,10 +7,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Applications/ConsumerApi/src/ConsumerApi.csproj b/Applications/ConsumerApi/src/ConsumerApi.csproj index 45e760791f..f54a3c7aba 100644 --- a/Applications/ConsumerApi/src/ConsumerApi.csproj +++ b/Applications/ConsumerApi/src/ConsumerApi.csproj @@ -5,21 +5,16 @@ - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - true - + + + + + + + + + + @@ -33,20 +28,20 @@ - - - - - - - - - - + + + + + + + + + + - - + + diff --git a/Applications/DatabaseMigrator/src/DatabaseMigrator/DatabaseMigrator.csproj b/Applications/DatabaseMigrator/src/DatabaseMigrator/DatabaseMigrator.csproj index ec9fe2e954..195f9f693a 100644 --- a/Applications/DatabaseMigrator/src/DatabaseMigrator/DatabaseMigrator.csproj +++ b/Applications/DatabaseMigrator/src/DatabaseMigrator/DatabaseMigrator.csproj @@ -6,56 +6,60 @@ - - - - - - - - - - - - - - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/BuildingBlocks/src/BuildingBlocks.Application/Extensions/DbUpdateExceptionExtensions.cs b/BuildingBlocks/src/BuildingBlocks.Application/Extensions/DbUpdateExceptionExtensions.cs index 45ad6038bd..1d52ac6bcf 100644 --- a/BuildingBlocks/src/BuildingBlocks.Application/Extensions/DbUpdateExceptionExtensions.cs +++ b/BuildingBlocks/src/BuildingBlocks.Application/Extensions/DbUpdateExceptionExtensions.cs @@ -16,7 +16,7 @@ public static bool HasReason(this DbUpdateException ex, DbUpdateExceptionReason { return reason switch { - DbUpdateExceptionReason.DuplicateIndex => ex.Message.Contains("Index") || ex.InnerException != null && ex.InnerException.Message.Contains("UNIQUE"), + DbUpdateExceptionReason.DuplicateIndex => ex.Message.Contains("Index") || ex.InnerException != null && ex.InnerException.Message.ToLower().Contains("unique"), DbUpdateExceptionReason.UniqueKeyViolation => ex.GetBaseException() is SqlException { Number: 2627 } // SqlServer or PostgresException { SqlState: "23505" }, _ => throw new ArgumentException("The given reason does not exist.") diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/Handler.cs index 82559719ac..f177e71e7c 100644 --- a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/Handler.cs +++ b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/Handler.cs @@ -1,8 +1,11 @@ using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.BuildingBlocks.Application.PushNotifications; +using Backbone.BuildingBlocks.Domain; +using Backbone.BuildingBlocks.Infrastructure.Exceptions; using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository; using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess; +using Backbone.Modules.Devices.Domain; using Backbone.Modules.Devices.Domain.Entities.Identities; using MediatR; @@ -27,7 +30,14 @@ public async Task Handle(StartDeletionProce var deletionProcess = identity.StartDeletionProcessAsOwner(_userContext.GetDeviceId()); - await _identitiesRepository.Update(identity, cancellationToken); + try + { + await _identitiesRepository.Update(identity, cancellationToken); + } + catch (OnlyOneActiveDeletionProcessAllowedException) + { + throw new DomainException(DomainErrors.OnlyOneActiveDeletionProcessAllowed()); + } await _notificationSender.SendNotification(identity.Address, new DeletionProcessStartedPushNotification(), cancellationToken); diff --git a/Modules/Devices/src/Devices.Application/OnlyOneActiveDeletionProcessAllowedException.cs b/Modules/Devices/src/Devices.Application/OnlyOneActiveDeletionProcessAllowedException.cs new file mode 100644 index 0000000000..982b4adde6 --- /dev/null +++ b/Modules/Devices/src/Devices.Application/OnlyOneActiveDeletionProcessAllowedException.cs @@ -0,0 +1,14 @@ +namespace Backbone.Modules.Devices.Application; + +public class OnlyOneActiveDeletionProcessAllowedException : Exception +{ + private const string MESSAGE = "Only one active deletion process is allowed."; + + public OnlyOneActiveDeletionProcessAllowedException() : base(MESSAGE) + { + } + + public OnlyOneActiveDeletionProcessAllowedException(Exception innerException) : base(MESSAGE, innerException) + { + } +} diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs index 1815aa299c..00dbda66ad 100644 --- a/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs +++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs @@ -13,6 +13,7 @@ public class IdentityDeletionProcess : Entity private IdentityDeletionProcess() { // This constructor is for EF Core only; initializing the properties with null is therefore not a problem + IdentityAddress = null!; _auditLog = null!; Id = null!; } @@ -20,6 +21,7 @@ private IdentityDeletionProcess() private IdentityDeletionProcess(IdentityAddress createdBy, DeletionProcessStatus status) { Id = IdentityDeletionProcessId.Generate(); + IdentityAddress = null!; CreatedAt = SystemTime.UtcNow; Status = status; @@ -31,6 +33,7 @@ private IdentityDeletionProcess(IdentityAddress createdBy, DeletionProcessStatus private IdentityDeletionProcess(IdentityAddress createdBy, DeviceId createdByDevice) { Id = IdentityDeletionProcessId.Generate(); + IdentityAddress = null!; CreatedAt = SystemTime.UtcNow; ApproveInternally(createdBy, createdByDevice); @@ -39,6 +42,9 @@ private IdentityDeletionProcess(IdentityAddress createdBy, DeviceId createdByDev } public IdentityDeletionProcessId Id { get; } + + private IdentityAddress IdentityAddress { get; } + public IReadOnlyList AuditLog => _auditLog; public DeletionProcessStatus Status { get; private set; } public DateTime CreatedAt { get; } diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240909071633_AddUniqueIndexOnActiveDeletionProcesses.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240909071633_AddUniqueIndexOnActiveDeletionProcesses.Designer.cs new file mode 100644 index 0000000000..e2cbcc6b5b --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240909071633_AddUniqueIndexOnActiveDeletionProcesses.Designer.cs @@ -0,0 +1,925 @@ +// +using System; +using Backbone.Modules.Devices.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.Postgres.Migrations +{ + [DbContext(typeof(DevicesDbContext))] + [Migration("20240909071633_AddUniqueIndexOnActiveDeletionProcesses")] + partial class AddUniqueIndexOnActiveDeletionProcesses + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Devices") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.PushNotifications.PnsRegistration", b => + { + b.Property("DeviceId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("AppId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DevicePushIdentifier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Environment") + .HasColumnType("integer"); + + b.Property("Handle") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("character varying(200)") + .IsFixedLength(false); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("DeviceId"); + + b.ToTable("PnsRegistrations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CanBeManuallyAssigned") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CanBeUsedAsDefaultForClient") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .IsUnicode(true) + .HasColumnType("character varying(30)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tiers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Challenge", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Challenges", "Challenges", t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("UserName") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CommunicationLanguage") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("character(2)") + .HasDefaultValue("en") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.ToTable("Devices", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Property("Address") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletionGracePeriodEndsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IdentityVersion") + .HasColumnType("smallint"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TierId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("TierIdBeforeDeletion") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Address"); + + b.HasIndex("ClientId"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("ClientId"), "hash"); + + b.HasIndex("TierId"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("TierId"), "hash"); + + b.ToTable("Identities", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("ApprovalReminder1SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovalReminder2SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovalReminder3SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancelledByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletionStartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodEndsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder1SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder2SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder3SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("RejectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RejectedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.HasIndex(new[] { "IdentityAddress" }, "IX_only_one_active_deletion_process") + .IsUnique() + .HasFilter("\"Status\" = 1"); + + b.ToTable("IdentityDeletionProcesses", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("AdditionalData") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceIdHash") + .HasColumnType("bytea"); + + b.Property("IdentityAddressHash") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("IdentityDeletionProcessId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("MessageKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("NewStatus") + .HasColumnType("integer"); + + b.Property("OldStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IdentityDeletionProcessId"); + + b.ToTable("IdentityDeletionProcessAuditLog", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("MaxIdentities") + .HasColumnType("integer"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.HasIndex("DefaultTier"); + + b.ToTable("OpenIddictApplications", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", 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", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "Devices"); + }); + + 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", "Devices"); + }); + + 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", "Devices"); + }); + + 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", "Devices"); + }); + + 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", "Devices"); + }); + + 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", "Devices"); + }); + + 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", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Device", "Device") + .WithOne("User") + .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", "DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", "Identity") + .WithMany("Devices") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Identity"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", null) + .WithMany("DeletionProcesses") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", null) + .WithMany("AuditLog") + .HasForeignKey("IdentityDeletionProcessId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", null) + .WithMany() + .HasForeignKey("DefaultTier") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + 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("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", 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("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Navigation("User") + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Navigation("DeletionProcesses"); + + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240909071633_AddUniqueIndexOnActiveDeletionProcesses.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240909071633_AddUniqueIndexOnActiveDeletionProcesses.cs new file mode 100644 index 0000000000..385e5d2cf9 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240909071633_AddUniqueIndexOnActiveDeletionProcesses.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.Postgres.Migrations +{ + /// + public partial class AddUniqueIndexOnActiveDeletionProcesses : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "IdentityAddress", + schema: "Devices", + table: "IdentityDeletionProcesses", + type: "character varying(80)", + unicode: false, + maxLength: 80, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(80)", + oldUnicode: false, + oldMaxLength: 80, + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_only_one_active_deletion_process", + schema: "Devices", + table: "IdentityDeletionProcesses", + column: "IdentityAddress", + unique: true, + filter: "\"Status\" = 1"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_only_one_active_deletion_process", + schema: "Devices", + table: "IdentityDeletionProcesses"); + + migrationBuilder.AlterColumn( + name: "IdentityAddress", + schema: "Devices", + table: "IdentityDeletionProcesses", + type: "character varying(80)", + unicode: false, + maxLength: 80, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(80)", + oldUnicode: false, + oldMaxLength: 80); + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs index f6635de1b6..afe4e7e765 100644 --- a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs @@ -336,6 +336,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("IdentityAddress") + .IsRequired() .HasMaxLength(80) .IsUnicode(false) .HasColumnType("character varying(80)") @@ -357,6 +358,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("IdentityAddress"); + b.HasIndex(new[] { "IdentityAddress" }, "IX_only_one_active_deletion_process") + .IsUnique() + .HasFilter("\"Status\" = 1"); + b.ToTable("IdentityDeletionProcesses", "Devices"); }); @@ -786,7 +791,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", null) .WithMany("DeletionProcesses") .HasForeignKey("IdentityAddress") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240909071602_AddUniqueIndexOnActiveDeletionProcesses.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240909071602_AddUniqueIndexOnActiveDeletionProcesses.Designer.cs new file mode 100644 index 0000000000..8436b9215c --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240909071602_AddUniqueIndexOnActiveDeletionProcesses.Designer.cs @@ -0,0 +1,928 @@ +// +using System; +using Backbone.Modules.Devices.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.SqlServer.Migrations +{ + [DbContext(typeof(DevicesDbContext))] + [Migration("20240909071602_AddUniqueIndexOnActiveDeletionProcesses")] + partial class AddUniqueIndexOnActiveDeletionProcesses + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Devices") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.PushNotifications.PnsRegistration", b => + { + b.Property("DeviceId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("AppId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DevicePushIdentifier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Environment") + .HasColumnType("int"); + + b.Property("Handle") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)") + .IsFixedLength(false); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("DeviceId"); + + b.ToTable("PnsRegistrations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CanBeManuallyAssigned") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("CanBeUsedAsDefaultForClient") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .IsUnicode(true) + .HasColumnType("nvarchar(30)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tiers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Challenge", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Challenges", "Challenges", t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CommunicationLanguage") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("char(2)") + .HasDefaultValue("en") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.ToTable("Devices", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Property("Address") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletionGracePeriodEndsAt") + .HasColumnType("datetime2"); + + b.Property("IdentityVersion") + .HasColumnType("tinyint"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TierId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("TierIdBeforeDeletion") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Address"); + + b.HasIndex("ClientId") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.HasIndex("TierId") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.ToTable("Identities", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("ApprovalReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CancelledAt") + .HasColumnType("datetime2"); + + b.Property("CancelledByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletionStartedAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodEndsAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("RejectedAt") + .HasColumnType("datetime2"); + + b.Property("RejectedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.HasIndex(new[] { "IdentityAddress" }, "IX_only_one_active_deletion_process") + .IsUnique() + .HasFilter("\"Status\" = 1"); + + b.ToTable("IdentityDeletionProcesses", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("AdditionalData") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceIdHash") + .HasColumnType("varbinary(max)"); + + b.Property("IdentityAddressHash") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("IdentityDeletionProcessId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("MessageKey") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NewStatus") + .HasColumnType("int"); + + b.Property("OldStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IdentityDeletionProcessId"); + + b.ToTable("IdentityDeletionProcessAuditLog", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ClientSecret") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DefaultTier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("JsonWebKeySet") + .HasColumnType("nvarchar(max)"); + + b.Property("MaxIdentities") + .HasColumnType("int"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Requirements") + .HasColumnType("nvarchar(max)"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique() + .HasFilter("[ClientId] IS NOT NULL"); + + b.HasIndex("DefaultTier"); + + b.ToTable("OpenIddictApplications", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", 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", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Descriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Resources") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[Name] IS NOT NULL"); + + b.ToTable("OpenIddictScopes", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("AuthorizationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedemptionDate") + .HasColumnType("datetime2"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasFilter("[ReferenceId] IS NOT NULL"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "Devices"); + }); + + 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", "Devices"); + }); + + 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", "Devices"); + }); + + 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", "Devices"); + }); + + 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", "Devices"); + }); + + 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", "Devices"); + }); + + 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", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Device", "Device") + .WithOne("User") + .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", "DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", "Identity") + .WithMany("Devices") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Identity"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", null) + .WithMany("DeletionProcesses") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", null) + .WithMany("AuditLog") + .HasForeignKey("IdentityDeletionProcessId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", null) + .WithMany() + .HasForeignKey("DefaultTier") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + 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("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", 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("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Navigation("User") + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Navigation("DeletionProcesses"); + + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240909071602_AddUniqueIndexOnActiveDeletionProcesses.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240909071602_AddUniqueIndexOnActiveDeletionProcesses.cs new file mode 100644 index 0000000000..82cf39a518 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240909071602_AddUniqueIndexOnActiveDeletionProcesses.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.SqlServer.Migrations +{ + /// + public partial class AddUniqueIndexOnActiveDeletionProcesses : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "IdentityAddress", + schema: "Devices", + table: "IdentityDeletionProcesses", + type: "varchar(80)", + unicode: false, + maxLength: 80, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "varchar(80)", + oldUnicode: false, + oldMaxLength: 80, + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_only_one_active_deletion_process", + schema: "Devices", + table: "IdentityDeletionProcesses", + column: "IdentityAddress", + unique: true, + filter: "\"Status\" = 1"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_only_one_active_deletion_process", + schema: "Devices", + table: "IdentityDeletionProcesses"); + + migrationBuilder.AlterColumn( + name: "IdentityAddress", + schema: "Devices", + table: "IdentityDeletionProcesses", + type: "varchar(80)", + unicode: false, + maxLength: 80, + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(80)", + oldUnicode: false, + oldMaxLength: 80); + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/DevicesDbContextModelSnapshot.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/DevicesDbContextModelSnapshot.cs index df2a9ae88f..021391f517 100644 --- a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/DevicesDbContextModelSnapshot.cs +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/DevicesDbContextModelSnapshot.cs @@ -335,6 +335,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime2"); b.Property("IdentityAddress") + .IsRequired() .HasMaxLength(80) .IsUnicode(false) .HasColumnType("varchar(80)") @@ -356,6 +357,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("IdentityAddress"); + b.HasIndex(new[] { "IdentityAddress" }, "IX_only_one_active_deletion_process") + .IsUnique() + .HasFilter("\"Status\" = 1"); + b.ToTable("IdentityDeletionProcesses", "Devices"); }); @@ -789,7 +794,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", null) .WithMany("DeletionProcesses") .HasForeignKey("IdentityAddress") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/IdentityDeletionProcessEntityTypeConfiguration.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/IdentityDeletionProcessEntityTypeConfiguration.cs index d02dc04b32..82d198eac8 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/IdentityDeletionProcessEntityTypeConfiguration.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/IdentityDeletionProcessEntityTypeConfiguration.cs @@ -19,6 +19,9 @@ public override void Configure(EntityTypeBuilder builde builder.Property(x => x.ApprovalReminder2SentAt); builder.Property(x => x.ApprovalReminder3SentAt); + builder.HasIndex("IdentityAddress"); // we have to explicitly define the default foreign-key index because otherwise the filtered index below overrides the default index + builder.HasIndex(["IdentityAddress"], "IX_only_one_active_deletion_process").IsUnique().HasFilter(@"""Status"" = 1"); + builder.HasMany(x => x.AuditLog).WithOne().OnDelete(DeleteBehavior.SetNull).IsRequired(false); builder.Ignore(x => x.HasApprovalPeriodExpired); builder.Ignore(x => x.HasGracePeriodExpired); diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs index fbef1f4e03..a095ffdabf 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs @@ -106,7 +106,16 @@ public async Task Update(Device device, CancellationToken cancellationToken) public async Task Update(Identity identity, CancellationToken cancellationToken) { _identities.Update(identity); - await _dbContext.SaveChangesAsync(cancellationToken); + try + { + await _dbContext.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException exception) + { + if (exception.HasReason(DbUpdateExceptionReason.DuplicateIndex) && exception.InnerException!.Message.Contains("IX_only_one_active_deletion_process")) + throw new OnlyOneActiveDeletionProcessAllowedException(exception); + throw; + } } public async Task> Find(Expression> filter, CancellationToken cancellationToken, bool track = false) diff --git a/scripts/windows/efcore/add_migration.ps1 b/scripts/windows/efcore/add_migration.ps1 index 9f04b3f367..d7199d9937 100644 --- a/scripts/windows/efcore/add_migration.ps1 +++ b/scripts/windows/efcore/add_migration.ps1 @@ -11,24 +11,20 @@ $provider = switch ($provider) { } $repoRoot = git rev-parse --show-toplevel $dbContextName = "${moduleName}DbContext" -$adminApiProject = "$repoRoot\Applications\AdminApi\src\AdminApi" -$consumerApiProject = "$repoRoot\Applications\ConsumerApi\src" -$startupProject = If ($moduleName -eq "AdminApi") { $adminApiProject } Else { $consumerApiProject } +$startupProject = "$repoRoot\Applications\DatabaseMigrator\src\DatabaseMigrator" function AddMigration { param ( $provider ) + New-Item env:"Infrastructure__SqlDatabase__Provider" -Value $provider -Force | Out-Null + switch ($moduleName) { "AdminApi" { - New-Item env:"Infrastructure__SqlDatabase__Provider" -Value $provider -Force | Out-Null - $migrationProject = "$repoRoot\Applications\AdminApi\src\AdminApi.Infrastructure.Database.$provider" } Default { - New-Item env:"Modules__${moduleName}__Infrastructure__SqlDatabase__Provider" -Value $provider -Force | Out-Null - $migrationProject = "$repoRoot\Modules\$moduleName\src\$moduleName.Infrastructure.Database.$provider" } } diff --git a/scripts/windows/efcore/remove_migration.ps1 b/scripts/windows/efcore/remove_migration.ps1 index 887db19e0f..1efc5ffc33 100644 --- a/scripts/windows/efcore/remove_migration.ps1 +++ b/scripts/windows/efcore/remove_migration.ps1 @@ -5,26 +5,21 @@ Param( $repoRoot = git rev-parse --show-toplevel $dbContextName = "${moduleName}DbContext" -$adminApiProject = "$repoRoot\Applications\AdminApi\src\AdminApi" -$consumerApiProject = "$repoRoot\Applications\ConsumerApi\src" +$startupProject = "$repoRoot\Applications\DatabaseMigrator\src\DatabaseMigrator" function RemoveMigration { param ( $provider ) + New-Item env:"Infrastructure__SqlDatabase__Provider" -Value $provider -Force | Out-Null + switch($moduleName){ "AdminApi" { - New-Item env:"Infrastructure__SqlDatabase__Provider" -Value $provider -Force | Out-Null - $migrationProject = "$repoRoot\AdminApi\src\AdminApi.Infrastructure.Database.$provider" - $startupProject = $adminApiProject } Default { - New-Item env:"Modules__${moduleName}__Infrastructure__SqlDatabase__Provider" -Value $provider -Force | Out-Null - $migrationProject = "$repoRoot\Modules\$moduleName\src\$moduleName.Infrastructure.Database.$provider" - $startupProject = $consumerApiProject } } diff --git a/scripts/windows/efcore/update_local_database.ps1 b/scripts/windows/efcore/update_local_database.ps1 index c76b49019a..172b965386 100644 --- a/scripts/windows/efcore/update_local_database.ps1 +++ b/scripts/windows/efcore/update_local_database.ps1 @@ -7,33 +7,28 @@ Param( $environment="dbmigrations-" + $provider.ToLower() $repoRoot = git rev-parse --show-toplevel $dbContextName = "${moduleName}DbContext" -$adminApiProject = "$repoRoot\Applications\AdminApi\src\AdminApi" -$consumerApiProject = "$repoRoot\Applications\ConsumerApi\src" +$startupProject = "$repoRoot\Applications\DatabaseMigrator\src\DatabaseMigrator" function UpdateLocalDatabase { param ( $provider ) + New-Item env:"Infrastructure__SqlDatabase__Provider" -Value $provider -Force | Out-Null + switch($moduleName){ "AdminApi"{ - New-Item env:"${moduleName}__Infrastructure__SqlDatabase__Provider" -Value $provider -Force | Out-Null - $migrationProject = "$repoRoot\AdminApi\src\AdminApi.Infrastructure.Database.$provider" - $startupProject = $adminApiProject } Default { - New-Item env:"Modules__${moduleName}__Infrastructure__SqlDatabase__Provider" -Value $provider -Force | Out-Null - $migrationProject = "$repoRoot\Modules\$moduleName\src\$moduleName.Infrastructure.Database.$provider" - $startupProject = $consumerApiProject } } $moduleNameLowercase = $moduleName.ToLower() switch ($provider) { - "SqlServer" { $connectionString = """Server=localhost;Database=enmeshed;User Id=$moduleNameLowercase;Password=Passw0rd;TrustServerCertificate=True""" } - "Postgres" { $connectionString = """Server=localhost;Database=enmeshed;User ID=$moduleNameLowercase;Password=Passw0rd""" } + "SqlServer" { $connectionString = """Server=localhost;Database=enmeshed;User Id=sa;Password=Passw0rd;TrustServerCertificate=True""" } + "Postgres" { $connectionString = """Server=localhost;Database=enmeshed;User ID=postgres;Password=admin""" } } $cmd = "dotnet ef database update $migrationName --startup-project $startupProject --project $migrationProject --verbose --context $dbContextName --connection $connectionString -- --environment $environment"