From 2f1105152992fd77678845b32704c97842409d73 Mon Sep 17 00:00:00 2001 From: Daniel Prokopowicz Date: Wed, 6 May 2026 12:21:53 +0200 Subject: [PATCH 1/6] Feature: Board UI refactoring, real-time comments and DND fixes --- server/Directory.Packages.props | 2 + .../Abstractions/Persistence/IAppDbContext.cs | 2 +- .../Boards/Create/CreateBoardCommand.cs | 2 +- .../Boards/Get/GetBoardsResponse.cs | 2 +- .../Boards/GetById/GetBoardByIdHandler.cs | 11 +- .../Boards/GetById/GetBoardByIdResponse.cs | 14 +- .../GetDetails/GetBoardDetailsResponse.cs | 2 +- .../Cards/AppComment/AddCardCommentCommand.cs | 7 + .../Cards/AppComment/AddCardCommentHandler.cs | 46 + .../Cards/AppComment/CardCommentDto.cs | 8 + .../GetByBoardId/GetCardsByBoardIdResponse.cs | 2 +- .../Cards/GetById/GetCardByIdResponse.cs | 2 +- .../GetByListId/GetCardsByListIdResponse.cs | 2 +- .../GetCardsBySwimlaneIdResponse.cs | 2 +- .../Cards/Update/UpdateCardCommand.cs | 2 +- server/src/Domain/Boards/Board.cs | 6 +- server/src/Domain/Boards/BoardEvents.cs | 2 +- server/src/Domain/Cards/Card.cs | 15 +- server/src/Domain/Cards/CardComment.cs | 24 + server/src/Domain/Cards/CardEvents.cs | 4 +- .../src/Infrastructure/Infrastructure.csproj | 4 + .../Persistence/AppDbContext.cs | 12 +- .../Configurations/CardConfiguration.cs | 3 + ...60505113154_AddCardDescription.Designer.cs | 1124 ++++++++++++++++ .../20260505113154_AddCardDescription.cs | 40 + ...20260505141557_AddCardComments.Designer.cs | 1186 +++++++++++++++++ .../20260505141557_AddCardComments.cs | 66 + ...193022_AddCardCommentsRelation.Designer.cs | 1186 +++++++++++++++++ .../20260505193022_AddCardCommentsRelation.cs | 22 + .../Migrations/AppDbContextModelSnapshot.cs | 66 +- .../Presentation/Endpoints/Cards/Update.cs | 2 +- server/src/Presentation/Hubs/Board/Hub.cs | 66 +- .../Hubs/Board/IBoardHubClient.cs | 13 +- server/src/Presentation/Presentation.csproj | 1 + web/src/lib/features/boards/api/boards.api.ts | 9 + .../boards/components/BoardCard.svelte | 37 +- .../features/boards/components/Card.svelte | 93 +- .../boards/components/CardModal.svelte | 77 +- .../features/boards/components/List.svelte | 113 +- .../boards/components/Swimlane.svelte | 183 +-- web/src/lib/features/boards/hub/boards.hub.ts | 13 +- .../lib/features/boards/types/boards.api.ts | 9 +- .../lib/features/boards/types/boards.hub.ts | 2 +- .../routes/(auth)/boards/[id]/+page.svelte | 25 +- web/src/routes/(auth)/profile/+page.svelte | 4 - 45 files changed, 4216 insertions(+), 297 deletions(-) create mode 100644 server/src/Application/Cards/AppComment/AddCardCommentCommand.cs create mode 100644 server/src/Application/Cards/AppComment/AddCardCommentHandler.cs create mode 100644 server/src/Application/Cards/AppComment/CardCommentDto.cs create mode 100644 server/src/Domain/Cards/CardComment.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.Designer.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.Designer.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.Designer.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.cs diff --git a/server/Directory.Packages.props b/server/Directory.Packages.props index 8199c09..a803184 100644 --- a/server/Directory.Packages.props +++ b/server/Directory.Packages.props @@ -9,10 +9,12 @@ + + diff --git a/server/src/Application/Abstractions/Persistence/IAppDbContext.cs b/server/src/Application/Abstractions/Persistence/IAppDbContext.cs index f5adbb6..cdf5682 100644 --- a/server/src/Application/Abstractions/Persistence/IAppDbContext.cs +++ b/server/src/Application/Abstractions/Persistence/IAppDbContext.cs @@ -28,6 +28,6 @@ public interface IAppDbContext DbSet Tags { get; } DbSet Cards { get; } - + DbSet CardComments { get; } Task SaveChangesAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/server/src/Application/Boards/Create/CreateBoardCommand.cs b/server/src/Application/Boards/Create/CreateBoardCommand.cs index 6a3a2e4..e84ed38 100644 --- a/server/src/Application/Boards/Create/CreateBoardCommand.cs +++ b/server/src/Application/Boards/Create/CreateBoardCommand.cs @@ -7,5 +7,5 @@ public sealed record CreateBoardMemberRequest(int UserId, MemberRole Role); public sealed record CreateBoardCommand( string Title, - string Description, + string? Description, IReadOnlyList? Members = null) : ICommand; \ No newline at end of file diff --git a/server/src/Application/Boards/Get/GetBoardsResponse.cs b/server/src/Application/Boards/Get/GetBoardsResponse.cs index aaac80c..9a10fc9 100644 --- a/server/src/Application/Boards/Get/GetBoardsResponse.cs +++ b/server/src/Application/Boards/Get/GetBoardsResponse.cs @@ -15,7 +15,7 @@ public sealed record UserDto(int Id, string UserName) public sealed record BoardDto( int Id, string Title, - string Description, + string? Description, MemberRole YourRole, DateTimeOffset CreatedAt, UserDto CreatedBy, diff --git a/server/src/Application/Boards/GetById/GetBoardByIdHandler.cs b/server/src/Application/Boards/GetById/GetBoardByIdHandler.cs index 14a8b0f..f78aab8 100644 --- a/server/src/Application/Boards/GetById/GetBoardByIdHandler.cs +++ b/server/src/Application/Boards/GetById/GetBoardByIdHandler.cs @@ -46,7 +46,16 @@ public async Task> Handle(GetBoardByIdQuery query, c.CreatedAt, UserDto.From(c.CreatedBy), c.UpdatedAt, - UserDto.From(c.UpdatedBy))) + UserDto.From(c.UpdatedBy), + c.Comments + .OrderBy(comm => comm.CreatedAt) + .Select(comm => new CardCommentDto( + comm.Id, + comm.UserId, + comm.User.UserName ?? "Unknown", + comm.Content, + comm.CreatedAt)) + .ToList())) .ToList())) .ToList())) .ToList())) diff --git a/server/src/Application/Boards/GetById/GetBoardByIdResponse.cs b/server/src/Application/Boards/GetById/GetBoardByIdResponse.cs index 0caa6b4..2ef7c35 100644 --- a/server/src/Application/Boards/GetById/GetBoardByIdResponse.cs +++ b/server/src/Application/Boards/GetById/GetBoardByIdResponse.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Snapflow.Application.Cards.AddComment; using static Snapflow.Application.Boards.GetById.GetBoardByIdResponse; namespace Snapflow.Application.Boards.GetById; @@ -6,7 +7,7 @@ namespace Snapflow.Application.Boards.GetById; public sealed record GetBoardByIdResponse( int Id, string Title, - string Description, + string? Description, IReadOnlyList Swimlanes) { public sealed record UserDto(int Id, string UserName) @@ -34,10 +35,17 @@ public sealed record ListDto( public sealed record CardDto( int Id, string Title, - string Description, + string? Description, string Rank, DateTimeOffset CreatedAt, UserDto CreatedBy, DateTimeOffset? UpdatedAt, - UserDto? UpdatedBy); + UserDto? UpdatedBy, + List Comments); + public sealed record CardCommentDto( + int Id, + int UserId, + string UserName, + string Content, + DateTimeOffset CreatedAt); } \ No newline at end of file diff --git a/server/src/Application/Boards/GetDetails/GetBoardDetailsResponse.cs b/server/src/Application/Boards/GetDetails/GetBoardDetailsResponse.cs index 0453e20..b56356b 100644 --- a/server/src/Application/Boards/GetDetails/GetBoardDetailsResponse.cs +++ b/server/src/Application/Boards/GetDetails/GetBoardDetailsResponse.cs @@ -5,7 +5,7 @@ namespace Snapflow.Application.Boards.GetDetails; public sealed record GetBoardDetailsResponse( int Id, string Title, - string Description, + string? Description, IReadOnlyList Members); public sealed record GetBoardDetailsMemberResponse( diff --git a/server/src/Application/Cards/AppComment/AddCardCommentCommand.cs b/server/src/Application/Cards/AppComment/AddCardCommentCommand.cs new file mode 100644 index 0000000..09d1d42 --- /dev/null +++ b/server/src/Application/Cards/AppComment/AddCardCommentCommand.cs @@ -0,0 +1,7 @@ +using Snapflow.Application.Abstractions.Messaging; +using Snapflow.Application.Boards.GetById; + +namespace Snapflow.Application.Cards.AddComment; + +public sealed record AddCardCommentCommand(int CardId, int UserId, string Content) + : ICommand; \ No newline at end of file diff --git a/server/src/Application/Cards/AppComment/AddCardCommentHandler.cs b/server/src/Application/Cards/AppComment/AddCardCommentHandler.cs new file mode 100644 index 0000000..f8e270a --- /dev/null +++ b/server/src/Application/Cards/AppComment/AddCardCommentHandler.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore; +using Snapflow.Application.Abstractions.Messaging; +using Snapflow.Application.Abstractions.Persistence; +using Snapflow.Application.Boards.GetById; +using Snapflow.Common; +using Snapflow.Domain.Cards; + +namespace Snapflow.Application.Cards.AddComment; + +internal sealed class AddCardCommentCommandHandler( + IAppDbContext context) : ICommandHandler // <--- ZMIANA 1: Zwracamy DTO zamiast int +{ + public async Task> Handle(AddCardCommentCommand request, CancellationToken cancellationToken = default) + { + var card = await context.Cards + .FirstOrDefaultAsync(c => c.Id == request.CardId && !c.IsDeleted, cancellationToken); + + if (card is null) + { + return Result.Failure(CardErrors.NotFound(request.CardId)); + } + + var user = await context.Users + .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken); + + if (user is null) + { + return Result.Failure(new Error("User.NotFound", "User not found", ErrorType.NotFound)); + } + + var comment = CardComment.Create(request.CardId, request.UserId, request.Content); + + context.CardComments.Add(comment); + await context.SaveChangesAsync(cancellationToken); + + var dto = new GetBoardByIdResponse.CardCommentDto( + comment.Id, + user.Id, + user.UserName ?? "Unknown", + comment.Content, + comment.CreatedAt + ); + + return dto; + } +} \ No newline at end of file diff --git a/server/src/Application/Cards/AppComment/CardCommentDto.cs b/server/src/Application/Cards/AppComment/CardCommentDto.cs new file mode 100644 index 0000000..da0eec8 --- /dev/null +++ b/server/src/Application/Cards/AppComment/CardCommentDto.cs @@ -0,0 +1,8 @@ +namespace Snapflow.Application.Cards.AddComment; + +public sealed record CardCommentDto( + int Id, + int UserId, + string UserName, + string Content, + DateTimeOffset CreatedAt); \ No newline at end of file diff --git a/server/src/Application/Cards/GetByBoardId/GetCardsByBoardIdResponse.cs b/server/src/Application/Cards/GetByBoardId/GetCardsByBoardIdResponse.cs index a96a3b6..efe9042 100644 --- a/server/src/Application/Cards/GetByBoardId/GetCardsByBoardIdResponse.cs +++ b/server/src/Application/Cards/GetByBoardId/GetCardsByBoardIdResponse.cs @@ -8,6 +8,6 @@ public sealed record CardDto( int SwimlaneId, int BoardId, string Title, - string Description, + string? Description, string Rank); } \ No newline at end of file diff --git a/server/src/Application/Cards/GetById/GetCardByIdResponse.cs b/server/src/Application/Cards/GetById/GetCardByIdResponse.cs index 1d97e80..bbcbba8 100644 --- a/server/src/Application/Cards/GetById/GetCardByIdResponse.cs +++ b/server/src/Application/Cards/GetById/GetCardByIdResponse.cs @@ -9,7 +9,7 @@ public sealed record GetCardByIdResponse( int SwimlaneId, int BoardId, string Title, - string Description, + string? Description, string Rank, DateTimeOffset CreatedAt, UserDto CreatedBy, diff --git a/server/src/Application/Cards/GetByListId/GetCardsByListIdResponse.cs b/server/src/Application/Cards/GetByListId/GetCardsByListIdResponse.cs index b7bd651..8f9658a 100644 --- a/server/src/Application/Cards/GetByListId/GetCardsByListIdResponse.cs +++ b/server/src/Application/Cards/GetByListId/GetCardsByListIdResponse.cs @@ -8,6 +8,6 @@ public sealed record CardDto( int SwimlaneId, int BoardId, string Title, - string Description, + string? Description, string Rank); } \ No newline at end of file diff --git a/server/src/Application/Cards/GetBySwimlaneId/GetCardsBySwimlaneIdResponse.cs b/server/src/Application/Cards/GetBySwimlaneId/GetCardsBySwimlaneIdResponse.cs index 050fd9a..2cac8db 100644 --- a/server/src/Application/Cards/GetBySwimlaneId/GetCardsBySwimlaneIdResponse.cs +++ b/server/src/Application/Cards/GetBySwimlaneId/GetCardsBySwimlaneIdResponse.cs @@ -8,6 +8,6 @@ public sealed record CardDto( int SwimlaneId, int BoardId, string Title, - string Description, + string? Description, string Rank); } \ No newline at end of file diff --git a/server/src/Application/Cards/Update/UpdateCardCommand.cs b/server/src/Application/Cards/Update/UpdateCardCommand.cs index 1c3183c..ab4844b 100644 --- a/server/src/Application/Cards/Update/UpdateCardCommand.cs +++ b/server/src/Application/Cards/Update/UpdateCardCommand.cs @@ -2,4 +2,4 @@ namespace Snapflow.Application.Cards.Update; -public sealed record UpdateCardCommand(int Id, string Title, string Description) : ICommand; \ No newline at end of file +public sealed record UpdateCardCommand(int Id, string Title, string? Description) : ICommand; \ No newline at end of file diff --git a/server/src/Domain/Boards/Board.cs b/server/src/Domain/Boards/Board.cs index 03beda6..8eb3e50 100644 --- a/server/src/Domain/Boards/Board.cs +++ b/server/src/Domain/Boards/Board.cs @@ -13,7 +13,7 @@ public class Board : Entity public Board() { } public string Title { get; private set; } = null!; - public string Description { get; private set; } = ""; + public string? Description { get; private set; } = ""; public DateTimeOffset CreatedAt { get; private set; } public int CreatedById { get; private set; } @@ -34,7 +34,7 @@ public Board() { } public virtual ICollection Cards { get; private set; } = []; public virtual ICollection Tags { get; private set; } = []; - public static Board Create(string title, string description, int createdById, DateTimeOffset createdAt, string? connectionId = null) + public static Board Create(string title, string? description, int createdById, DateTimeOffset createdAt, string? connectionId = null) { var board = new Board { @@ -51,7 +51,7 @@ public static Board Create(string title, string description, int createdById, Da return board; } - public void Update(string title, string description, int updatedById, DateTimeOffset updatedAt, string? connectionId = null) + public void Update(string title, string? description, int updatedById, DateTimeOffset updatedAt, string? connectionId = null) { Title = title; Description = description; diff --git a/server/src/Domain/Boards/BoardEvents.cs b/server/src/Domain/Boards/BoardEvents.cs index 9cb8d5e..a47ee34 100644 --- a/server/src/Domain/Boards/BoardEvents.cs +++ b/server/src/Domain/Boards/BoardEvents.cs @@ -11,7 +11,7 @@ public sealed record BoardCreatedDomainEvent( public sealed record BoardUpdatedDomainEvent( int Id, string Title, - string Description, + string? Description, string? ConnectionId) : IDomainEvent; public sealed record BoardDeletedDomainEvent( diff --git a/server/src/Domain/Cards/Card.cs b/server/src/Domain/Cards/Card.cs index 3cbfdb5..9ed3cec 100644 --- a/server/src/Domain/Cards/Card.cs +++ b/server/src/Domain/Cards/Card.cs @@ -20,9 +20,8 @@ public Card() { } public virtual List List { get; private set; } = null!; public string Title { get; private set; } = null!; - public string Description { get; private set; } = ""; + public string? Description { get; private set; } = ""; public string Rank { get; set; } = null!; - public DateTimeOffset CreatedAt { get; private set; } public int CreatedById { get; private set; } public virtual IUser CreatedBy { get; private set; } = null!; @@ -38,8 +37,10 @@ public Card() { } public bool DeletedByCascade { get; private set; } public virtual ICollection Tags { get; private set; } = []; - - public static Card Create(int boardId, int swimlaneId, int listId, string title, string description, string rank, int createdById, DateTimeOffset createdAt, string? connectionId = null) + + private readonly List _comments = []; + public virtual IReadOnlyCollection Comments => _comments.AsReadOnly(); + public static Card Create(int boardId, int swimlaneId, int listId, string title, string? description, string rank, int createdById, DateTimeOffset createdAt, string? connectionId = null) { var card = new Card { @@ -58,7 +59,7 @@ public static Card Create(int boardId, int swimlaneId, int listId, string title, return card; } - public void Update(string title, string description, int updatedById, DateTimeOffset updatedAt, string? connectionId = null) + public void Update(string title, string? description, int updatedById, DateTimeOffset updatedAt, string? connectionId = null) { Title = title; Description = description; @@ -88,4 +89,8 @@ public void SoftDelete(int deletedById, DateTimeOffset deletedAt, string? connec Raise(c => new CardDeletedDomainEvent(Id, BoardId, connectionId)); } + public void AddComment(int userId, string content) + { + _comments.Add(CardComment.Create(Id, userId, content)); + } } \ No newline at end of file diff --git a/server/src/Domain/Cards/CardComment.cs b/server/src/Domain/Cards/CardComment.cs new file mode 100644 index 0000000..f699c95 --- /dev/null +++ b/server/src/Domain/Cards/CardComment.cs @@ -0,0 +1,24 @@ +using Snapflow.Domain.Users; +using Snapflow.Common; + +namespace Snapflow.Domain.Cards; + +public class CardComment : Entity +{ + public int CardId { get; private set; } + public int UserId { get; private set; } + public string Content { get; private set; } = null!; + public DateTimeOffset CreatedAt { get; private set; } + public virtual IUser User { get; private set; } = null!; + + public static CardComment Create(int cardId, int userId, string content) + { + return new CardComment + { + CardId = cardId, + UserId = userId, + Content = content, + CreatedAt = DateTimeOffset.UtcNow + }; + } +} \ No newline at end of file diff --git a/server/src/Domain/Cards/CardEvents.cs b/server/src/Domain/Cards/CardEvents.cs index d5434d7..36ea785 100644 --- a/server/src/Domain/Cards/CardEvents.cs +++ b/server/src/Domain/Cards/CardEvents.cs @@ -8,7 +8,7 @@ public sealed record CardCreatedDomainEvent( int SwimlaneId, int ListId, string Title, - string Description, + string? Description, string Rank, string? ConnectionId) : IDomainEvent; @@ -16,7 +16,7 @@ public sealed record CardUpdatedDomainEvent( int Id, int BoardId, string Title, - string Description, + string? Description, string? ConnectionId) : IDomainEvent; public sealed record CardMovedDomainEvent( diff --git a/server/src/Infrastructure/Infrastructure.csproj b/server/src/Infrastructure/Infrastructure.csproj index 7504989..b55c3c0 100644 --- a/server/src/Infrastructure/Infrastructure.csproj +++ b/server/src/Infrastructure/Infrastructure.csproj @@ -10,6 +10,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/server/src/Infrastructure/Persistence/AppDbContext.cs b/server/src/Infrastructure/Persistence/AppDbContext.cs index 56dcbd6..9c26774 100644 --- a/server/src/Infrastructure/Persistence/AppDbContext.cs +++ b/server/src/Infrastructure/Persistence/AppDbContext.cs @@ -14,6 +14,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Snapflow.Domain.Roles; + namespace Snapflow.Infrastructure.Persistence; public sealed class AppDbContext( @@ -28,7 +29,7 @@ public sealed class AppDbContext( public DbSet Members { get; private set; } public DbSet Tags { get; private set; } public DbSet Cards { get; private set; } - + public DbSet CardComments => Set(); public DbSet DataProtectionKeys { get; private set; } protected override void OnModelCreating(ModelBuilder builder) @@ -44,5 +45,14 @@ protected override void OnModelCreating(ModelBuilder builder) { builder.Entity(entityType.ClrType).Ignore(nameof(IEntity.DomainEvents)); } + builder.Entity(entity => + { + entity.HasOne(x => (AppUser)x.User) + .WithMany() + .HasForeignKey(x => x.UserId) + .OnDelete(DeleteBehavior.NoAction); + + entity.ToTable("CardComments"); + }); } } diff --git a/server/src/Infrastructure/Persistence/Configurations/CardConfiguration.cs b/server/src/Infrastructure/Persistence/Configurations/CardConfiguration.cs index 1811120..a05e952 100644 --- a/server/src/Infrastructure/Persistence/Configurations/CardConfiguration.cs +++ b/server/src/Infrastructure/Persistence/Configurations/CardConfiguration.cs @@ -37,5 +37,8 @@ public void Configure(EntityTypeBuilder builder) builder.HasIndex(c => c.ListId) .HasFilter("is_deleted = false"); + builder.Property(card => card.Description) + .HasMaxLength(2000) + .IsRequired(false); } } diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.Designer.cs b/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.Designer.cs new file mode 100644 index 0000000..2f9e8c0 --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.Designer.cs @@ -0,0 +1,1124 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Snapflow.Infrastructure.Persistence; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260505113154_AddCardDescription")] + partial class AddCardDescription + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CardTag", b => + { + b.Property("CardsId") + .HasColumnType("integer") + .HasColumnName("cards_id"); + + b.Property("TagsId") + .HasColumnType("integer") + .HasColumnName("tags_id"); + + b.HasKey("CardsId", "TagsId") + .HasName("pk_card_tag"); + + b.HasIndex("TagsId") + .HasDatabaseName("ix_card_tag_tags_id"); + + b.ToTable("card_tag", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property("Xml") + .HasColumnType("text") + .HasColumnName("xml"); + + b.HasKey("Id") + .HasName("pk_data_protection_keys"); + + b.ToTable("data_protection_keys", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_role_claims_role_id"); + + b.ToTable("role_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_claims_user_id"); + + b.ToTable("user_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("text") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_logins_user_id"); + + b.ToTable("user_logins", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_user_roles_role_id"); + + b.ToTable("user_roles", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_user_tokens"); + + b.ToTable("user_tokens", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasDefaultValue("") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_boards"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_boards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_boards_deleted_by_id"); + + b.HasIndex("Title") + .HasDatabaseName("ix_boards_title") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Title"), "GIN"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_boards_updated_by_id"); + + b.ToTable("boards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("ListId") + .HasColumnType("integer") + .HasColumnName("list_id"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_cards"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_cards_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_cards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_cards_deleted_by_id"); + + b.HasIndex("ListId") + .HasDatabaseName("ix_cards_list_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_cards_swimlane_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_cards_updated_by_id"); + + b.HasIndex("ListId", "Rank") + .IsUnique() + .HasDatabaseName("ix_cards_list_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("cards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.Property("Width") + .HasColumnType("integer") + .HasColumnName("width"); + + b.HasKey("Id") + .HasName("pk_lists"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_lists_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_lists_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_lists_deleted_by_id"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_lists_swimlane_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_lists_updated_by_id"); + + b.HasIndex("SwimlaneId", "Rank") + .IsUnique() + .HasDatabaseName("ix_lists_swimlane_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("lists", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("BoardId", "UserId") + .HasName("pk_board_members"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_board_members_user_id"); + + b.HasIndex("BoardId", "Role") + .IsUnique() + .HasDatabaseName("ix_board_members_board_id_role") + .HasFilter("\"role\" = 0"); + + b.ToTable("board_members", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Height") + .HasColumnType("integer") + .HasColumnName("height"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_swimlanes"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_swimlanes_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_swimlanes_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_swimlanes_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_swimlanes_updated_by_id"); + + b.HasIndex("BoardId", "Rank") + .IsUnique() + .HasDatabaseName("ix_swimlanes_board_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("swimlanes", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("Color") + .HasColumnType("integer") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_tags"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_tags_board_id"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_tags_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_tags_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_tags_updated_by_id"); + + b.ToTable("tags", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("roles", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasColumnName("access_failed_count"); + + b.Property("AvatarContentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("avatar_content_type"); + + b.Property("AvatarData") + .HasColumnType("bytea") + .HasColumnName("avatar_data"); + + b.Property("AvatarType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("avatar_type"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("avatar_url"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasColumnName("email_confirmed"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_user_name"); + + b.Property("PasswordHash") + .HasColumnType("text") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("text") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("text") + .HasColumnName("security_stamp"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_users_email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", "public"); + }); + + modelBuilder.Entity("CardTag", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany() + .HasForeignKey("CardsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_cards_cards_id"); + + b.HasOne("Snapflow.Domain.Tags.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_tags_tags_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_role_claims_roles_role_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_claims_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_logins_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_roles_role_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_tokens_users_user_id"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_boards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_updated_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Cards") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_cards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Lists.List", "List") + .WithMany("Cards") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_lists_list_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Cards") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("List"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Lists") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_lists_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Lists") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Members") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_board_members_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_board_members_users_user_id"); + + b.Navigation("Board"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Swimlanes") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_swimlanes_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_swimlanes_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Tags") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tags_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tags_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + + b.Navigation("Members"); + + b.Navigation("Swimlanes"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Navigation("Cards"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.cs b/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.cs new file mode 100644 index 0000000..79f161c --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddCardDescription : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "description", + schema: "public", + table: "cards", + type: "character varying(2000)", + maxLength: 2000, + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "description", + schema: "public", + table: "cards", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(2000)", + oldMaxLength: 2000, + oldNullable: true); + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.Designer.cs b/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.Designer.cs new file mode 100644 index 0000000..cf8aebc --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.Designer.cs @@ -0,0 +1,1186 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Snapflow.Infrastructure.Persistence; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260505141557_AddCardComments")] + partial class AddCardComments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CardTag", b => + { + b.Property("CardsId") + .HasColumnType("integer") + .HasColumnName("cards_id"); + + b.Property("TagsId") + .HasColumnType("integer") + .HasColumnName("tags_id"); + + b.HasKey("CardsId", "TagsId") + .HasName("pk_card_tag"); + + b.HasIndex("TagsId") + .HasDatabaseName("ix_card_tag_tags_id"); + + b.ToTable("card_tag", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property("Xml") + .HasColumnType("text") + .HasColumnName("xml"); + + b.HasKey("Id") + .HasName("pk_data_protection_keys"); + + b.ToTable("data_protection_keys", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_role_claims_role_id"); + + b.ToTable("role_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_claims_user_id"); + + b.ToTable("user_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("text") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_logins_user_id"); + + b.ToTable("user_logins", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_user_roles_role_id"); + + b.ToTable("user_roles", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_user_tokens"); + + b.ToTable("user_tokens", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasDefaultValue("") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_boards"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_boards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_boards_deleted_by_id"); + + b.HasIndex("Title") + .HasDatabaseName("ix_boards_title") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Title"), "GIN"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_boards_updated_by_id"); + + b.ToTable("boards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("ListId") + .HasColumnType("integer") + .HasColumnName("list_id"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_cards"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_cards_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_cards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_cards_deleted_by_id"); + + b.HasIndex("ListId") + .HasDatabaseName("ix_cards_list_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_cards_swimlane_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_cards_updated_by_id"); + + b.HasIndex("ListId", "Rank") + .IsUnique() + .HasDatabaseName("ix_cards_list_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("cards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CardId") + .HasColumnType("integer") + .HasColumnName("card_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_card_comments"); + + b.HasIndex("CardId") + .HasDatabaseName("ix_card_comments_card_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_card_comments_user_id"); + + b.ToTable("CardComments", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.Property("Width") + .HasColumnType("integer") + .HasColumnName("width"); + + b.HasKey("Id") + .HasName("pk_lists"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_lists_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_lists_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_lists_deleted_by_id"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_lists_swimlane_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_lists_updated_by_id"); + + b.HasIndex("SwimlaneId", "Rank") + .IsUnique() + .HasDatabaseName("ix_lists_swimlane_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("lists", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("BoardId", "UserId") + .HasName("pk_board_members"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_board_members_user_id"); + + b.HasIndex("BoardId", "Role") + .IsUnique() + .HasDatabaseName("ix_board_members_board_id_role") + .HasFilter("\"role\" = 0"); + + b.ToTable("board_members", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Height") + .HasColumnType("integer") + .HasColumnName("height"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_swimlanes"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_swimlanes_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_swimlanes_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_swimlanes_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_swimlanes_updated_by_id"); + + b.HasIndex("BoardId", "Rank") + .IsUnique() + .HasDatabaseName("ix_swimlanes_board_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("swimlanes", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("Color") + .HasColumnType("integer") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_tags"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_tags_board_id"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_tags_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_tags_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_tags_updated_by_id"); + + b.ToTable("tags", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("roles", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasColumnName("access_failed_count"); + + b.Property("AvatarContentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("avatar_content_type"); + + b.Property("AvatarData") + .HasColumnType("bytea") + .HasColumnName("avatar_data"); + + b.Property("AvatarType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("avatar_type"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("avatar_url"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasColumnName("email_confirmed"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_user_name"); + + b.Property("PasswordHash") + .HasColumnType("text") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("text") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("text") + .HasColumnName("security_stamp"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_users_email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", "public"); + }); + + modelBuilder.Entity("CardTag", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany() + .HasForeignKey("CardsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_cards_cards_id"); + + b.HasOne("Snapflow.Domain.Tags.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_tags_tags_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_role_claims_roles_role_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_claims_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_logins_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_roles_role_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_tokens_users_user_id"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_boards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_updated_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Cards") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_cards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Lists.List", "List") + .WithMany("Cards") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_lists_list_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Cards") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("List"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany("Comments") + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_comments_cards_card_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired() + .HasConstraintName("fk_card_comments_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Lists") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_lists_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Lists") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Members") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_board_members_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_board_members_users_user_id"); + + b.Navigation("Board"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Swimlanes") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_swimlanes_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_swimlanes_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Tags") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tags_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tags_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + + b.Navigation("Members"); + + b.Navigation("Swimlanes"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Navigation("Cards"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.cs b/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.cs new file mode 100644 index 0000000..2a9c69c --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddCardComments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CardComments", + schema: "public", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + card_id = table.Column(type: "integer", nullable: false), + user_id = table.Column(type: "integer", nullable: false), + content = table.Column(type: "text", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_card_comments", x => x.id); + table.ForeignKey( + name: "fk_card_comments_cards_card_id", + column: x => x.card_id, + principalSchema: "public", + principalTable: "cards", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_card_comments_users_user_id", + column: x => x.user_id, + principalSchema: "public", + principalTable: "users", + principalColumn: "id"); + }); + + migrationBuilder.CreateIndex( + name: "ix_card_comments_card_id", + schema: "public", + table: "CardComments", + column: "card_id"); + + migrationBuilder.CreateIndex( + name: "ix_card_comments_user_id", + schema: "public", + table: "CardComments", + column: "user_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CardComments", + schema: "public"); + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.Designer.cs b/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.Designer.cs new file mode 100644 index 0000000..07cb54c --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.Designer.cs @@ -0,0 +1,1186 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Snapflow.Infrastructure.Persistence; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260505193022_AddCardCommentsRelation")] + partial class AddCardCommentsRelation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CardTag", b => + { + b.Property("CardsId") + .HasColumnType("integer") + .HasColumnName("cards_id"); + + b.Property("TagsId") + .HasColumnType("integer") + .HasColumnName("tags_id"); + + b.HasKey("CardsId", "TagsId") + .HasName("pk_card_tag"); + + b.HasIndex("TagsId") + .HasDatabaseName("ix_card_tag_tags_id"); + + b.ToTable("card_tag", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property("Xml") + .HasColumnType("text") + .HasColumnName("xml"); + + b.HasKey("Id") + .HasName("pk_data_protection_keys"); + + b.ToTable("data_protection_keys", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_role_claims_role_id"); + + b.ToTable("role_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_claims_user_id"); + + b.ToTable("user_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("text") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_logins_user_id"); + + b.ToTable("user_logins", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_user_roles_role_id"); + + b.ToTable("user_roles", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_user_tokens"); + + b.ToTable("user_tokens", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasDefaultValue("") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_boards"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_boards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_boards_deleted_by_id"); + + b.HasIndex("Title") + .HasDatabaseName("ix_boards_title") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Title"), "GIN"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_boards_updated_by_id"); + + b.ToTable("boards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("ListId") + .HasColumnType("integer") + .HasColumnName("list_id"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_cards"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_cards_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_cards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_cards_deleted_by_id"); + + b.HasIndex("ListId") + .HasDatabaseName("ix_cards_list_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_cards_swimlane_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_cards_updated_by_id"); + + b.HasIndex("ListId", "Rank") + .IsUnique() + .HasDatabaseName("ix_cards_list_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("cards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CardId") + .HasColumnType("integer") + .HasColumnName("card_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_card_comments"); + + b.HasIndex("CardId") + .HasDatabaseName("ix_card_comments_card_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_card_comments_user_id"); + + b.ToTable("CardComments", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.Property("Width") + .HasColumnType("integer") + .HasColumnName("width"); + + b.HasKey("Id") + .HasName("pk_lists"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_lists_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_lists_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_lists_deleted_by_id"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_lists_swimlane_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_lists_updated_by_id"); + + b.HasIndex("SwimlaneId", "Rank") + .IsUnique() + .HasDatabaseName("ix_lists_swimlane_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("lists", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("BoardId", "UserId") + .HasName("pk_board_members"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_board_members_user_id"); + + b.HasIndex("BoardId", "Role") + .IsUnique() + .HasDatabaseName("ix_board_members_board_id_role") + .HasFilter("\"role\" = 0"); + + b.ToTable("board_members", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Height") + .HasColumnType("integer") + .HasColumnName("height"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_swimlanes"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_swimlanes_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_swimlanes_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_swimlanes_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_swimlanes_updated_by_id"); + + b.HasIndex("BoardId", "Rank") + .IsUnique() + .HasDatabaseName("ix_swimlanes_board_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("swimlanes", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("Color") + .HasColumnType("integer") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_tags"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_tags_board_id"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_tags_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_tags_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_tags_updated_by_id"); + + b.ToTable("tags", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("roles", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasColumnName("access_failed_count"); + + b.Property("AvatarContentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("avatar_content_type"); + + b.Property("AvatarData") + .HasColumnType("bytea") + .HasColumnName("avatar_data"); + + b.Property("AvatarType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("avatar_type"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("avatar_url"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasColumnName("email_confirmed"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_user_name"); + + b.Property("PasswordHash") + .HasColumnType("text") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("text") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("text") + .HasColumnName("security_stamp"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_users_email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", "public"); + }); + + modelBuilder.Entity("CardTag", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany() + .HasForeignKey("CardsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_cards_cards_id"); + + b.HasOne("Snapflow.Domain.Tags.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_tags_tags_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_role_claims_roles_role_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_claims_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_logins_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_roles_role_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_tokens_users_user_id"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_boards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_updated_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Cards") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_cards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Lists.List", "List") + .WithMany("Cards") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_lists_list_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Cards") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("List"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany("Comments") + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_comments_cards_card_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired() + .HasConstraintName("fk_card_comments_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Lists") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_lists_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Lists") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Members") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_board_members_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_board_members_users_user_id"); + + b.Navigation("Board"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Swimlanes") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_swimlanes_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_swimlanes_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Tags") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tags_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tags_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + + b.Navigation("Members"); + + b.Navigation("Swimlanes"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Navigation("Cards"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.cs b/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.cs new file mode 100644 index 0000000..3dfc4f3 --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddCardCommentsRelation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs b/server/src/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs index c4a67ac..220a163 100644 --- a/server/src/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/server/src/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -301,8 +301,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("deleted_by_id"); b.Property("Description") - .IsRequired() - .HasColumnType("text") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") .HasColumnName("description"); b.Property("IsDeleted") @@ -367,6 +367,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("cards", "public"); }); + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CardId") + .HasColumnType("integer") + .HasColumnName("card_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_card_comments"); + + b.HasIndex("CardId") + .HasDatabaseName("ix_card_comments_card_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_card_comments_user_id"); + + b.ToTable("CardComments", "public"); + }); + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => { b.Property("Id") @@ -950,6 +988,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("UpdatedBy"); }); + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany("Comments") + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_comments_cards_card_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired() + .HasConstraintName("fk_card_comments_users_user_id"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => { b.HasOne("Snapflow.Domain.Boards.Board", "Board") @@ -1104,6 +1161,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Tags"); }); + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Navigation("Comments"); + }); + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => { b.Navigation("Cards"); diff --git a/server/src/Presentation/Endpoints/Cards/Update.cs b/server/src/Presentation/Endpoints/Cards/Update.cs index 7bee44f..404a97a 100644 --- a/server/src/Presentation/Endpoints/Cards/Update.cs +++ b/server/src/Presentation/Endpoints/Cards/Update.cs @@ -8,7 +8,7 @@ namespace Snapflow.Presentation.Endpoints.Cards; internal sealed class Update : IEndpoint { - public sealed record UpdateCardRequest(string Title, string Description); + public sealed record UpdateCardRequest(string Title, string? Description); public void MapEndpoint(IEndpointRouteBuilder app) { diff --git a/server/src/Presentation/Hubs/Board/Hub.cs b/server/src/Presentation/Hubs/Board/Hub.cs index 8f0ff1a..60b0064 100644 --- a/server/src/Presentation/Hubs/Board/Hub.cs +++ b/server/src/Presentation/Hubs/Board/Hub.cs @@ -1,6 +1,15 @@ -using Microsoft.AspNetCore.Authorization; +using System.Globalization; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; +using Snapflow.Application.Abstractions.Messaging; +using Snapflow.Application.Boards.GetById; +using Snapflow.Application.Cards.AddComment; +using Snapflow.Common; using Snapflow.Domain.Boards; +using Snapflow.Presentation.Extensions; +using Microsoft.Extensions.DependencyInjection; namespace Snapflow.Presentation.Hubs.Board; @@ -17,13 +26,30 @@ public override async Task OnConnectedAsync() Context.Abort(); return; } - if (!httpContext.Request.RouteValues.TryGetValue("boardId", out var boardIdObj) || - !int.TryParse(boardIdObj?.ToString(), out var boardId)) + + int boardId = 0; + + if (httpContext.Request.RouteValues.TryGetValue("boardId", out var boardIdObj) && int.TryParse(boardIdObj?.ToString(), out var parsedRouteId)) + { + boardId = parsedRouteId; + } + else + { + var pathSegments = httpContext.Request.Path.Value?.Split('/'); + var idSegment = pathSegments?.FirstOrDefault(s => int.TryParse(s, out _)); + if (idSegment != null) + { + boardId = int.Parse(idSegment, CultureInfo.InvariantCulture); + } + } + + if (boardId == 0) { - logger.LogWarning("Connection {ConnectionId} aborted: invalid or missing boardId in route.", Context.ConnectionId); + logger.LogWarning("Connection {ConnectionId} aborted: could not find boardId in URL {Url}.", Context.ConnectionId, httpContext.Request.Path.Value); Context.Abort(); return; } + Context.SetBoardId(boardId); var userIdString = Context.UserIdentifier; await Groups.AddToGroupAsync(Context.ConnectionId, $"{boardId}", Context.ConnectionAborted); @@ -37,4 +63,34 @@ public override async Task OnConnectedAsync() if (logger.IsEnabled(LogLevel.Information)) logger.LogInformation("Connection {ConnectionId} connected to board {BoardId}.", Context.ConnectionId, boardId); } -} + + public async Task AddComment( + int cardId, + string content, + [FromServices] ICommandHandler handler) + { + // 1. Pobieranie ID użytkownika + if (!int.TryParse(Context.UserIdentifier, out int userId)) return; + + var command = new AddCardCommentCommand(cardId, userId, content); + + // 2. Używamy konkretnego handlera do obsłużenia komendy (wywołujemy .Handle zamiast .Send) + var result = await handler.Handle(command, default); + + // 3. Sprawdzamy wynik + if (result.IsSuccess && result.Value is not null) + { + var httpContext = Context.GetHttpContext(); + + if (httpContext?.Request.RouteValues.TryGetValue("boardId", out var boardIdObj) == true && boardIdObj is not null) + { + await Clients.Group(boardIdObj.ToString()!).CommentAdded(cardId, result.Value); + } + } + else + { + logger.LogWarning("Failed to add comment to card {CardId}. Error: {Error}", cardId, result.Error); + } + } +} +public sealed record AddCardCommentRequest(int CardId, string Content); \ No newline at end of file diff --git a/server/src/Presentation/Hubs/Board/IBoardHubClient.cs b/server/src/Presentation/Hubs/Board/IBoardHubClient.cs index 1cf8db1..26a9dc7 100644 --- a/server/src/Presentation/Hubs/Board/IBoardHubClient.cs +++ b/server/src/Presentation/Hubs/Board/IBoardHubClient.cs @@ -1,10 +1,11 @@ -using Snapflow.Domain.Members; - +using Snapflow.Application.Boards.GetById; +using Snapflow.Application.Cards.AddComment; +using Snapflow.Domain.Members; namespace Snapflow.Presentation.Hubs.Board; public interface IBoardHubClient { - public sealed record BoardUpdatedPayload(string Title, string Description); + public sealed record BoardUpdatedPayload(string Title, string? Description); Task BoardUpdated(BoardUpdatedPayload payload, CancellationToken cancellationToken = default); @@ -46,11 +47,11 @@ public sealed record ListDeletedPayload(int Id); Task CardUnlocked(); - public sealed record CardCreatedPayload(int Id, int ListId, string Title, string Description, string Rank); + public sealed record CardCreatedPayload(int Id, int ListId, string Title, string? Description, string Rank); Task CardCreated(CardCreatedPayload payload, CancellationToken cancellationToken = default); - - public sealed record CardUpdatedPayload(int Id, string Title, string Description); + Task CommentAdded(int cardId, GetBoardByIdResponse.CardCommentDto comment); + public sealed record CardUpdatedPayload(int Id, string Title, string? Description); Task CardUpdated(CardUpdatedPayload payload, CancellationToken cancellationToken = default); diff --git a/server/src/Presentation/Presentation.csproj b/server/src/Presentation/Presentation.csproj index cdb0333..d8c6d9e 100644 --- a/server/src/Presentation/Presentation.csproj +++ b/server/src/Presentation/Presentation.csproj @@ -7,6 +7,7 @@ + diff --git a/web/src/lib/features/boards/api/boards.api.ts b/web/src/lib/features/boards/api/boards.api.ts index 193b02a..00c738a 100644 --- a/web/src/lib/features/boards/api/boards.api.ts +++ b/web/src/lib/features/boards/api/boards.api.ts @@ -136,4 +136,13 @@ export class BoardsService { }) ); } + addComment(cardId: number, content: string): Promise> { + return this.handleResponse( + this.apiClient.fetch(`cards/${cardId}/comments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content }) + }) + ); + } } diff --git a/web/src/lib/features/boards/components/BoardCard.svelte b/web/src/lib/features/boards/components/BoardCard.svelte index 3ed5fe7..e9beffc 100644 --- a/web/src/lib/features/boards/components/BoardCard.svelte +++ b/web/src/lib/features/boards/components/BoardCard.svelte @@ -18,31 +18,34 @@
-
-

{title}

+ + +
+

+ {title} +

+ {#if editHref && yourRole?.toLowerCase() === 'owner'} -
+
+ +
- {#if card.description} -

- {card.description} -

- {/if} - -
-
+ +
+ +
{card.createdBy.userName.charAt(0).toUpperCase()}
+ + + {#if card.comments && card.comments.length > 0} +
+ + {card.comments.length} +
+ {/if}
-
- - {new Date(card.createdAt).toLocaleDateString()} + + +
+ + + {new Date(card.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} +
-
+
\ No newline at end of file diff --git a/web/src/lib/features/boards/components/CardModal.svelte b/web/src/lib/features/boards/components/CardModal.svelte index 9359f0d..d5c7218 100644 --- a/web/src/lib/features/boards/components/CardModal.svelte +++ b/web/src/lib/features/boards/components/CardModal.svelte @@ -4,6 +4,7 @@ import type { GetBoardByIdResponse } from '$lib/features/boards/types/boards.api'; import type { Response } from '$lib/core/types/app'; import { createForm } from '$lib/ui/utils'; + let { open = $bindable(false), card = $bindable(undefined), @@ -16,7 +17,8 @@ mobileDrawerSide = 'bottom', triggerElement = undefined, onConfirm, - onDelete + onDelete, + onAddComment }: { open: boolean; card?: GetBoardByIdResponse.CardDto; @@ -30,10 +32,27 @@ triggerElement?: HTMLElement | null; onConfirm: (title: string, description: string) => Promise>; onDelete?: (id: number) => Promise; + onAddComment?: (content: string) => Promise; } = $props(); let isDeleting = $state(false); + let newComment = $state(''); + let isSendingComment = $state(false); + async function handleAddComment() { + if (!card || !newComment.trim() || isSendingComment || !onAddComment) return; + + isSendingComment = true; + try { + await onAddComment(newComment); + + newComment = ''; + } catch (error) { + console.error("Failed to add comment:", error); + } finally { + isSendingComment = false; + } + } const form = createForm({ initialValues: { title: '', @@ -103,7 +122,7 @@ {desktopAnimation} {mobileAnimation} {triggerElement} - contentClass="sm:rounded-lg md:w-full" + contentClass="sm:rounded-lg md:w-full hide-scrollbar" >
+ + {#if card} +
+

Comments

+ + +
+ {#if card.comments && card.comments.length > 0} + {#each card.comments as comment} +
+
+ {comment.userName} + {new Date(comment.createdAt).toLocaleString()} +
+

{comment.content}

+
+ {/each} + {:else} +

No comments yet. Be the first!

+ {/if} +
+ + +
+