From 00229fd406606c97f965500bcbe92fae02a9e393 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 1 Jun 2025 19:18:23 +0800 Subject: [PATCH] :boom: :recycle: Refactor cloud files' references, and loading system --- DysonNetwork.Sphere/Account/Account.cs | 7 +- .../Account/AccountCurrentController.cs | 49 +- .../Activity/ActivityService.cs | 2 - DysonNetwork.Sphere/AppDatabase.cs | 10 +- DysonNetwork.Sphere/Chat/ChatController.cs | 3 +- DysonNetwork.Sphere/Chat/ChatRoom.cs | 10 +- .../Chat/ChatRoomController.cs | 70 +- DysonNetwork.Sphere/Chat/ChatService.cs | 51 +- DysonNetwork.Sphere/Chat/Message.cs | 32 +- ...032_RefactorCloudFileReference.Designer.cs | 3328 +++++++++++++++++ ...250601111032_RefactorCloudFileReference.cs | 557 +++ .../Migrations/AppDatabaseModelSnapshot.cs | 281 +- DysonNetwork.Sphere/Post/Post.cs | 12 +- DysonNetwork.Sphere/Post/PostController.cs | 5 - DysonNetwork.Sphere/Post/PostService.cs | 53 +- DysonNetwork.Sphere/Program.cs | 1 + DysonNetwork.Sphere/Publisher/Publisher.cs | 7 +- .../Publisher/PublisherController.cs | 54 +- .../Publisher/PublisherService.cs | 48 +- DysonNetwork.Sphere/Realm/Realm.cs | 11 +- DysonNetwork.Sphere/Realm/RealmController.cs | 88 +- DysonNetwork.Sphere/Sticker/Sticker.cs | 7 +- .../Sticker/StickerController.cs | 2 +- DysonNetwork.Sphere/Sticker/StickerService.cs | 52 +- DysonNetwork.Sphere/Storage/CacheService.cs | 6 + DysonNetwork.Sphere/Storage/CloudFile.cs | 71 +- .../Storage/CloudFileUnusedRecyclingJob.cs | 127 + .../Storage/FileExpirationJob.cs | 66 + .../Storage/FileReferenceService.cs | 407 ++ DysonNetwork.Sphere/Storage/FileService.cs | 319 +- DysonNetwork.Sphere/Storage/ICloudFile.cs | 49 + DysonNetwork.sln.DotSettings.user | 1 + 32 files changed, 5204 insertions(+), 582 deletions(-) create mode 100644 DysonNetwork.Sphere/Migrations/20250601111032_RefactorCloudFileReference.Designer.cs create mode 100644 DysonNetwork.Sphere/Migrations/20250601111032_RefactorCloudFileReference.cs create mode 100644 DysonNetwork.Sphere/Storage/CloudFileUnusedRecyclingJob.cs create mode 100644 DysonNetwork.Sphere/Storage/FileExpirationJob.cs create mode 100644 DysonNetwork.Sphere/Storage/FileReferenceService.cs create mode 100644 DysonNetwork.Sphere/Storage/ICloudFile.cs diff --git a/DysonNetwork.Sphere/Account/Account.cs b/DysonNetwork.Sphere/Account/Account.cs index a2fdbc9..47704d6 100644 --- a/DysonNetwork.Sphere/Account/Account.cs +++ b/DysonNetwork.Sphere/Account/Account.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; using DysonNetwork.Sphere.Permission; +using DysonNetwork.Sphere.Storage; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -68,10 +69,8 @@ public class Profile : ModelBase (Experience - Leveling.ExperiencePerLevel[Level]) * 100.0 / (Leveling.ExperiencePerLevel[Level + 1] - Leveling.ExperiencePerLevel[Level]); - [MaxLength(32)] public string? PictureId { get; set; } - public Storage.CloudFile? Picture { get; set; } - [MaxLength(32)] public string? BackgroundId { get; set; } - public Storage.CloudFile? Background { get; set; } + [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; } + [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; } public Guid AccountId { get; set; } [JsonIgnore] public Account Account { get; set; } = null!; diff --git a/DysonNetwork.Sphere/Account/AccountCurrentController.cs b/DysonNetwork.Sphere/Account/AccountCurrentController.cs index 85b5d97..debf145 100644 --- a/DysonNetwork.Sphere/Account/AccountCurrentController.cs +++ b/DysonNetwork.Sphere/Account/AccountCurrentController.cs @@ -16,10 +16,13 @@ public class AccountCurrentController( AppDatabase db, AccountService accounts, FileService fs, + FileReferenceService fileRefService, AccountEventService events, AuthService auth ) : ControllerBase { + private const string ProfilePictureFileUsageIdentifier = "profile"; + [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetCurrentIdentity() @@ -90,22 +93,52 @@ public class AccountCurrentController( { var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync(); if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); - if (profile.Picture is not null) - await fs.MarkUsageAsync(profile.Picture, -1); - profile.Picture = picture; - await fs.MarkUsageAsync(picture, 1); + var profileResourceId = $"profile:{profile.Id}"; + + // Remove old references for the profile picture + if (profile.Picture is not null) { + var oldPictureRefs = await fileRefService.GetResourceReferencesAsync(profileResourceId, ProfilePictureFileUsageIdentifier); + foreach (var oldRef in oldPictureRefs) + { + await fileRefService.DeleteReferenceAsync(oldRef.Id); + } + } + + profile.Picture = picture.ToReferenceObject(); + + // Create new reference + await fileRefService.CreateReferenceAsync( + picture.Id, + ProfilePictureFileUsageIdentifier, + profileResourceId + ); } if (request.BackgroundId is not null) { var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync(); if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); - if (profile.Background is not null) - await fs.MarkUsageAsync(profile.Background, -1); - profile.Background = background; - await fs.MarkUsageAsync(background, 1); + var profileResourceId = $"profile:{profile.Id}"; + + // Remove old references for the profile background + if (profile.Background is not null) { + var oldBackgroundRefs = await fileRefService.GetResourceReferencesAsync(profileResourceId, ProfilePictureFileUsageIdentifier); + foreach (var oldRef in oldBackgroundRefs) + { + await fileRefService.DeleteReferenceAsync(oldRef.Id); + } + } + + profile.Background = background.ToReferenceObject(); + + // Create new reference + await fileRefService.CreateReferenceAsync( + background.Id, + ProfilePictureFileUsageIdentifier, + profileResourceId + ); } db.Update(profile); diff --git a/DysonNetwork.Sphere/Activity/ActivityService.cs b/DysonNetwork.Sphere/Activity/ActivityService.cs index 9596cb3..98c1f3e 100644 --- a/DysonNetwork.Sphere/Activity/ActivityService.cs +++ b/DysonNetwork.Sphere/Activity/ActivityService.cs @@ -18,9 +18,7 @@ public class ActivityReaderService(AppDatabase db, PostService ps) if (postsId.Count > 0) { var posts = await db.Posts.Where(e => postsId.Contains(e.Id)) - .Include(e => e.ThreadedPost) .Include(e => e.ForwardedPost) - .Include(e => e.Attachments) .Include(e => e.Categories) .Include(e => e.Tags) .FilterWithVisibility(currentUser, userFriends) diff --git a/DysonNetwork.Sphere/AppDatabase.cs b/DysonNetwork.Sphere/AppDatabase.cs index 272cc37..b2e6c89 100644 --- a/DysonNetwork.Sphere/AppDatabase.cs +++ b/DysonNetwork.Sphere/AppDatabase.cs @@ -10,6 +10,11 @@ using Quartz; namespace DysonNetwork.Sphere; +public interface IIdentifiedResource +{ + public string ResourceIdentifier { get; } +} + public abstract class ModelBase { public Instant CreatedAt { get; set; } @@ -43,6 +48,7 @@ public class AppDatabase( public DbSet AuthChallenges { get; set; } public DbSet Files { get; set; } + public DbSet FileReferences { get; set; } public DbSet Activities { get; set; } @@ -178,10 +184,6 @@ public class AppDatabase( .HasGeneratedTsVectorColumn(p => p.SearchVector, "simple", p => new { p.Title, p.Description, p.Content }) .HasIndex(p => p.SearchVector) .HasMethod("GIN"); - modelBuilder.Entity() - .HasOne(p => p.ThreadedPost) - .WithOne() - .HasForeignKey(p => p.ThreadedPostId); modelBuilder.Entity() .HasOne(p => p.RepliedPost) .WithMany() diff --git a/DysonNetwork.Sphere/Chat/ChatController.cs b/DysonNetwork.Sphere/Chat/ChatController.cs index 035c50f..31ef5d1 100644 --- a/DysonNetwork.Sphere/Chat/ChatController.cs +++ b/DysonNetwork.Sphere/Chat/ChatController.cs @@ -92,7 +92,6 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ .Include(m => m.Sender) .Include(m => m.Sender.Account) .Include(m => m.Sender.Account.Profile) - .Include(m => m.Attachments) .Skip(offset) .Take(take) .ToListAsync(); @@ -169,6 +168,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ .ToListAsync(); message.Attachments = attachments .OrderBy(f => request.AttachmentsId.IndexOf(f.Id)) + .Select(f => f.ToReferenceObject()) .ToList(); } @@ -270,7 +270,6 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ var message = await db.ChatMessages .Include(m => m.Sender) .Include(m => m.ChatRoom) - .Include(m => m.Attachments) .FirstOrDefaultAsync(m => m.Id == messageId && m.ChatRoomId == roomId); if (message == null) return NotFound(); diff --git a/DysonNetwork.Sphere/Chat/ChatRoom.cs b/DysonNetwork.Sphere/Chat/ChatRoom.cs index 5a73fd9..5172c26 100644 --- a/DysonNetwork.Sphere/Chat/ChatRoom.cs +++ b/DysonNetwork.Sphere/Chat/ChatRoom.cs @@ -12,7 +12,7 @@ public enum ChatRoomType DirectMessage } -public class ChatRoom : ModelBase +public class ChatRoom : ModelBase, IIdentifiedResource { public Guid Id { get; set; } [MaxLength(1024)] public string? Name { get; set; } @@ -21,10 +21,8 @@ public class ChatRoom : ModelBase public bool IsCommunity { get; set; } public bool IsPublic { get; set; } - [MaxLength(32)] public string? PictureId { get; set; } - public CloudFile? Picture { get; set; } - [MaxLength(32)] public string? BackgroundId { get; set; } - public CloudFile? Background { get; set; } + [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; } + [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; } [JsonIgnore] public ICollection Members { get; set; } = new List(); @@ -35,6 +33,8 @@ public class ChatRoom : ModelBase [JsonPropertyName("members")] public ICollection DirectMembers { get; set; } = new List(); + + public string ResourceIdentifier => $"chatroom/{Id}"; } public enum ChatMemberRole diff --git a/DysonNetwork.Sphere/Chat/ChatRoomController.cs b/DysonNetwork.Sphere/Chat/ChatRoomController.cs index df5a704..83ab10a 100644 --- a/DysonNetwork.Sphere/Chat/ChatRoomController.cs +++ b/DysonNetwork.Sphere/Chat/ChatRoomController.cs @@ -17,6 +17,7 @@ namespace DysonNetwork.Sphere.Chat; public class ChatRoomController( AppDatabase db, FileService fs, + FileReferenceService fileRefService, ChatRoomService crs, RealmService rs, ActionLogService als, @@ -176,13 +177,13 @@ public class ChatRoomController( if (request.PictureId is not null) { - chatRoom.Picture = await db.Files.FindAsync(request.PictureId); + chatRoom.Picture = (await db.Files.FindAsync(request.PictureId))?.ToReferenceObject(); if (chatRoom.Picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); } if (request.BackgroundId is not null) { - chatRoom.Background = await db.Files.FindAsync(request.BackgroundId); + chatRoom.Background = (await db.Files.FindAsync(request.BackgroundId))?.ToReferenceObject(); if (chatRoom.Background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); } @@ -190,10 +191,21 @@ public class ChatRoomController( db.ChatRooms.Add(chatRoom); await db.SaveChangesAsync(); + var chatRoomResourceId = $"chatroom:{chatRoom.Id}"; + if (chatRoom.Picture is not null) - await fs.MarkUsageAsync(chatRoom.Picture, 1); + await fileRefService.CreateReferenceAsync( + chatRoom.Picture.Id, + "chat.room.picture", + chatRoomResourceId + ); + if (chatRoom.Background is not null) - await fs.MarkUsageAsync(chatRoom.Background, 1); + await fileRefService.CreateReferenceAsync( + chatRoom.Background.Id, + "chat.room.background", + chatRoomResourceId + ); als.CreateActionLogFromRequest( ActionLogType.ChatroomCreate, @@ -235,22 +247,50 @@ public class ChatRoomController( chatRoom.RealmId = member.RealmId; } + var chatRoomResourceId = $"chatroom:{chatRoom.Id}"; + if (request.PictureId is not null) { var picture = await db.Files.FindAsync(request.PictureId); if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); - await fs.MarkUsageAsync(picture, 1); - if (chatRoom.Picture is not null) await fs.MarkUsageAsync(chatRoom.Picture, -1); - chatRoom.Picture = picture; + + // Remove old references for pictures + var oldPictureRefs = await fileRefService.GetResourceReferencesAsync(chatRoomResourceId, "chat.room.picture"); + foreach (var oldRef in oldPictureRefs) + { + await fileRefService.DeleteReferenceAsync(oldRef.Id); + } + + // Add a new reference + await fileRefService.CreateReferenceAsync( + picture.Id, + "chat.room.picture", + chatRoomResourceId + ); + + chatRoom.Picture = picture.ToReferenceObject(); } if (request.BackgroundId is not null) { var background = await db.Files.FindAsync(request.BackgroundId); if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); - await fs.MarkUsageAsync(background, 1); - if (chatRoom.Background is not null) await fs.MarkUsageAsync(chatRoom.Background, -1); - chatRoom.Background = background; + + // Remove old references for backgrounds + var oldBackgroundRefs = await fileRefService.GetResourceReferencesAsync(chatRoomResourceId, "chat.room.background"); + foreach (var oldRef in oldBackgroundRefs) + { + await fileRefService.DeleteReferenceAsync(oldRef.Id); + } + + // Add a new reference + await fileRefService.CreateReferenceAsync( + background.Id, + "chat.room.background", + chatRoomResourceId + ); + + chatRoom.Background = background.ToReferenceObject(); } if (request.Name is not null) @@ -293,14 +333,14 @@ public class ChatRoomController( else if (!await crs.IsMemberWithRole(chatRoom.Id, currentUser.Id, ChatMemberRole.Owner)) return StatusCode(403, "You need at least be the owner to delete the chat."); + var chatRoomResourceId = $"chatroom:{chatRoom.Id}"; + + // Delete all file references for this chat room + await fileRefService.DeleteResourceReferencesAsync(chatRoomResourceId); + db.ChatRooms.Remove(chatRoom); await db.SaveChangesAsync(); - if (chatRoom.Picture is not null) - await fs.MarkUsageAsync(chatRoom.Picture, -1); - if (chatRoom.Background is not null) - await fs.MarkUsageAsync(chatRoom.Background, -1); - als.CreateActionLogFromRequest( ActionLogType.ChatroomDelete, new Dictionary { { "chatroom_id", chatRoom.Id } }, Request diff --git a/DysonNetwork.Sphere/Chat/ChatService.cs b/DysonNetwork.Sphere/Chat/ChatService.cs index 25a215e..028a8c4 100644 --- a/DysonNetwork.Sphere/Chat/ChatService.cs +++ b/DysonNetwork.Sphere/Chat/ChatService.cs @@ -10,6 +10,7 @@ namespace DysonNetwork.Sphere.Chat; public class ChatService( AppDatabase db, FileService fs, + FileReferenceService fileRefService, IServiceScopeFactory scopeFactory, IRealtimeService realtime, ILogger logger @@ -30,9 +31,16 @@ public class ChatService( var files = message.Attachments.Distinct().ToList(); if (files.Count != 0) { - await fs.MarkUsageRangeAsync(files, 1); - await fs.SetExpiresRangeAsync(files, Duration.FromDays(30)); - await fs.SetUsageRangeAsync(files, ChatFileUsageIdentifier); + var messageResourceId = $"message:{message.Id}"; + foreach (var file in files) + { + await fileRefService.CreateReferenceAsync( + file.Id, + ChatFileUsageIdentifier, + messageResourceId, + duration: Duration.FromDays(30) + ); + } } // Then start the delivery process @@ -64,7 +72,7 @@ public class ChatService( { message.Sender = sender; message.ChatRoom = room; - + using var scope = scopeFactory.CreateScope(); var scopedWs = scope.ServiceProvider.GetRequiredService(); var scopedNty = scope.ServiceProvider.GetRequiredService(); @@ -87,8 +95,8 @@ public class ChatService( .Where(a => a.MimeType != null && a.MimeType.StartsWith("image")) .Select(a => a.Id).ToList() }; - if (sender.Account.Profile is not { PictureId: null }) - metaDict["pfp"] = sender.Account.Profile.PictureId; + if (sender.Account.Profile is not { Picture: null }) + metaDict["pfp"] = sender.Account.Profile.Picture.Id; if (!string.IsNullOrEmpty(room.Name)) metaDict["room_name"] = room.Name; @@ -346,9 +354,28 @@ public class ChatService( if (attachmentsId is not null) { - message.Attachments = (await fs.DiffAndMarkFilesAsync(attachmentsId, message.Attachments)).current; - await fs.DiffAndSetExpiresAsync(attachmentsId, Duration.FromDays(30), message.Attachments); - await fs.DiffAndSetUsageAsync(attachmentsId, ChatFileUsageIdentifier, message.Attachments); + var messageResourceId = $"message:{message.Id}"; + + // Delete existing references for this message + await fileRefService.DeleteResourceReferencesAsync(messageResourceId); + + // Create new references for each attachment + foreach (var fileId in attachmentsId) + { + await fileRefService.CreateReferenceAsync( + fileId, + ChatFileUsageIdentifier, + messageResourceId, + duration: Duration.FromDays(30) + ); + } + + // Update message attachments by getting files from database + var files = await db.Files + .Where(f => attachmentsId.Contains(f.Id)) + .ToListAsync(); + + message.Attachments = files.Select(x => x.ToReferenceObject()).ToList(); } message.EditedAt = SystemClock.Instance.GetCurrentInstant(); @@ -371,9 +398,9 @@ public class ChatService( /// The message to delete public async Task DeleteMessageAsync(Message message) { - var files = message.Attachments.Distinct().ToList(); - if (files.Count != 0) - await fs.MarkUsageRangeAsync(files, -1); + // Remove all file references for this message + var messageResourceId = $"message:{message.Id}"; + await fileRefService.DeleteResourceReferencesAsync(messageResourceId); db.ChatMessages.Remove(message); await db.SaveChangesAsync(); diff --git a/DysonNetwork.Sphere/Chat/Message.cs b/DysonNetwork.Sphere/Chat/Message.cs index d050018..1f12238 100644 --- a/DysonNetwork.Sphere/Chat/Message.cs +++ b/DysonNetwork.Sphere/Chat/Message.cs @@ -7,7 +7,7 @@ using NodaTime; namespace DysonNetwork.Sphere.Chat; -public class Message : ModelBase +public class Message : ModelBase, IIdentifiedResource { public Guid Id { get; set; } = Guid.NewGuid(); [MaxLength(1024)] public string Type { get; set; } = null!; @@ -17,7 +17,7 @@ public class Message : ModelBase [MaxLength(36)] public string Nonce { get; set; } = null!; public Instant? EditedAt { get; set; } - public ICollection Attachments { get; set; } = new List(); + [Column(TypeName = "jsonb")] public List Attachments { get; set; } = []; public ICollection Reactions { get; set; } = new List(); public Guid? RepliedMessageId { get; set; } @@ -29,32 +29,8 @@ public class Message : ModelBase public ChatMember Sender { get; set; } = null!; public Guid ChatRoomId { get; set; } [JsonIgnore] public ChatRoom ChatRoom { get; set; } = null!; - - public Message Clone() - { - return new Message - { - Id = Id, - Content = Content, - Meta = Meta?.ToDictionary(entry => entry.Key, entry => entry.Value), - MembersMentioned = MembersMentioned?.ToList(), - Nonce = Nonce, - EditedAt = EditedAt, - Attachments = new List(Attachments), - Reactions = new List(Reactions), - RepliedMessageId = RepliedMessageId, - RepliedMessage = RepliedMessage?.Clone() as Message, - ForwardedMessageId = ForwardedMessageId, - ForwardedMessage = ForwardedMessage?.Clone() as Message, - SenderId = SenderId, - Sender = Sender, - ChatRoomId = ChatRoomId, - ChatRoom = ChatRoom, - CreatedAt = CreatedAt, - UpdatedAt = UpdatedAt, - DeletedAt = DeletedAt - }; - } + + public string ResourceIdentifier => $"message/{Id}"; } public enum MessageReactionAttitude diff --git a/DysonNetwork.Sphere/Migrations/20250601111032_RefactorCloudFileReference.Designer.cs b/DysonNetwork.Sphere/Migrations/20250601111032_RefactorCloudFileReference.Designer.cs new file mode 100644 index 0000000..0ab0474 --- /dev/null +++ b/DysonNetwork.Sphere/Migrations/20250601111032_RefactorCloudFileReference.Designer.cs @@ -0,0 +1,3328 @@ +// +using System; +using System.Collections.Generic; +using System.Text.Json; +using DysonNetwork.Sphere; +using DysonNetwork.Sphere.Account; +using DysonNetwork.Sphere.Storage; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace DysonNetwork.Sphere.Migrations +{ + [DbContext(typeof(AppDatabase))] + [Migration("20250601111032_RefactorCloudFileReference")] + partial class RefactorCloudFileReference + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsSuperuser") + .HasColumnType("boolean") + .HasColumnName("is_superuser"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("language"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Nick") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("nick"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_accounts_name"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountAuthFactor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Secret") + .HasMaxLength(8196) + .HasColumnType("character varying(8196)") + .HasColumnName("secret"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_auth_factors"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_auth_factors_account_id"); + + b.ToTable("account_auth_factors", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_account_contacts"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_contacts_account_id"); + + b.ToTable("account_contacts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.ActionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("action"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IpAddress") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ip_address"); + + b.Property("Location") + .HasColumnType("geometry") + .HasColumnName("location"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("SessionId") + .HasColumnType("uuid") + .HasColumnName("session_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_action_logs"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_action_logs_account_id"); + + b.HasIndex("SessionId") + .HasDatabaseName("ix_action_logs_session_id"); + + b.ToTable("action_logs", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Badge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Caption") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("caption"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_badges"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_badges_account_id"); + + b.ToTable("badges", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.CheckInResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("level"); + + b.Property("RewardExperience") + .HasColumnType("integer") + .HasColumnName("reward_experience"); + + b.Property("RewardPoints") + .HasColumnType("numeric") + .HasColumnName("reward_points"); + + b.Property>("Tips") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("tips"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_check_in_results"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_check_in_results_account_id"); + + b.ToTable("account_check_in_results", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.MagicSpell", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Spell") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("spell"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_magic_spells"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_magic_spells_account_id"); + + b.HasIndex("Spell") + .IsUnique() + .HasDatabaseName("ix_magic_spells_spell"); + + b.ToTable("magic_spells", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Content") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("Subtitle") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("subtitle"); + + b.Property("Title") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("title"); + + b.Property("Topic") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("topic"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("ViewedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("viewed_at"); + + b.HasKey("Id") + .HasName("pk_notifications"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_notifications_account_id"); + + b.ToTable("notifications", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.NotificationPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("device_id"); + + b.Property("DeviceToken") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("device_token"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_at"); + + b.Property("Provider") + .HasColumnType("integer") + .HasColumnName("provider"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_notification_push_subscriptions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_notification_push_subscriptions_account_id"); + + b.HasIndex("DeviceToken", "DeviceId") + .IsUnique() + .HasDatabaseName("ix_notification_push_subscriptions_device_token_device_id"); + + b.ToTable("notification_push_subscriptions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("bio"); + + b.Property("Birthday") + .HasColumnType("timestamp with time zone") + .HasColumnName("birthday"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Experience") + .HasColumnType("integer") + .HasColumnName("experience"); + + b.Property("FirstName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("first_name"); + + b.Property("Gender") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("gender"); + + b.Property("LastName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("last_name"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_at"); + + b.Property("MiddleName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("middle_name"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("Pronouns") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("pronouns"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_profiles"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_account_profiles_account_id"); + + b.ToTable("account_profiles", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Relationship", b => + { + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("RelatedId") + .HasColumnType("uuid") + .HasColumnName("related_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Status") + .HasColumnType("smallint") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("AccountId", "RelatedId") + .HasName("pk_account_relationships"); + + b.HasIndex("RelatedId") + .HasDatabaseName("ix_account_relationships_related_id"); + + b.ToTable("account_relationships", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Status", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Attitude") + .HasColumnType("integer") + .HasColumnName("attitude"); + + b.Property("ClearedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("cleared_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsInvisible") + .HasColumnType("boolean") + .HasColumnName("is_invisible"); + + b.Property("IsNotDisturb") + .HasColumnType("boolean") + .HasColumnName("is_not_disturb"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_statuses"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_statuses_account_id"); + + b.ToTable("account_statuses", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Activity.Activity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("ResourceIdentifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("resource_identifier"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property>("UsersVisible") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("users_visible"); + + b.Property("Visibility") + .HasColumnType("integer") + .HasColumnName("visibility"); + + b.HasKey("Id") + .HasName("pk_activities"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_activities_account_id"); + + b.ToTable("activities", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Auth.Challenge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("Audiences") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("audiences"); + + b.Property>("BlacklistFactors") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("blacklist_factors"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeviceId") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("device_id"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("FailedAttempts") + .HasColumnType("integer") + .HasColumnName("failed_attempts"); + + b.Property("IpAddress") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ip_address"); + + b.Property("Location") + .HasColumnType("geometry") + .HasColumnName("location"); + + b.Property("Nonce") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("nonce"); + + b.Property("Platform") + .HasColumnType("integer") + .HasColumnName("platform"); + + b.Property>("Scopes") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("scopes"); + + b.Property("StepRemain") + .HasColumnType("integer") + .HasColumnName("step_remain"); + + b.Property("StepTotal") + .HasColumnType("integer") + .HasColumnName("step_total"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_auth_challenges"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_challenges_account_id"); + + b.ToTable("auth_challenges", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Auth.Session", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ChallengeId") + .HasColumnType("uuid") + .HasColumnName("challenge_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property("LastGrantedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_granted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_auth_sessions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_sessions_account_id"); + + b.HasIndex("ChallengeId") + .HasDatabaseName("ix_auth_sessions_challenge_id"); + + b.ToTable("auth_sessions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.ChatMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ChatRoomId") + .HasColumnType("uuid") + .HasColumnName("chat_room_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsBot") + .HasColumnType("boolean") + .HasColumnName("is_bot"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_at"); + + b.Property("LastReadAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_at"); + + b.Property("LeaveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("leave_at"); + + b.Property("Nick") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("nick"); + + b.Property("Notify") + .HasColumnType("integer") + .HasColumnName("notify"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_chat_members"); + + b.HasAlternateKey("ChatRoomId", "AccountId") + .HasName("ak_chat_members_chat_room_id_account_id"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_chat_members_account_id"); + + b.ToTable("chat_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.ChatRoom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("IsCommunity") + .HasColumnType("boolean") + .HasColumnName("is_community"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_chat_rooms"); + + b.HasIndex("RealmId") + .HasDatabaseName("ix_chat_rooms_realm_id"); + + b.ToTable("chat_rooms", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property>("Attachments") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("attachments"); + + b.Property("ChatRoomId") + .HasColumnType("uuid") + .HasColumnName("chat_room_id"); + + b.Property("Content") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("edited_at"); + + b.Property("ForwardedMessageId") + .HasColumnType("uuid") + .HasColumnName("forwarded_message_id"); + + b.Property>("MembersMentioned") + .HasColumnType("jsonb") + .HasColumnName("members_mentioned"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Nonce") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("character varying(36)") + .HasColumnName("nonce"); + + b.Property("RepliedMessageId") + .HasColumnType("uuid") + .HasColumnName("replied_message_id"); + + b.Property("SenderId") + .HasColumnType("uuid") + .HasColumnName("sender_id"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_chat_messages"); + + b.HasIndex("ChatRoomId") + .HasDatabaseName("ix_chat_messages_chat_room_id"); + + b.HasIndex("ForwardedMessageId") + .HasDatabaseName("ix_chat_messages_forwarded_message_id"); + + b.HasIndex("RepliedMessageId") + .HasDatabaseName("ix_chat_messages_replied_message_id"); + + b.HasIndex("SenderId") + .HasDatabaseName("ix_chat_messages_sender_id"); + + b.ToTable("chat_messages", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.MessageReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Attitude") + .HasColumnType("integer") + .HasColumnName("attitude"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("MessageId") + .HasColumnType("uuid") + .HasColumnName("message_id"); + + b.Property("SenderId") + .HasColumnType("uuid") + .HasColumnName("sender_id"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("symbol"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_chat_reactions"); + + b.HasIndex("MessageId") + .HasDatabaseName("ix_chat_reactions_message_id"); + + b.HasIndex("SenderId") + .HasDatabaseName("ix_chat_reactions_sender_id"); + + b.ToTable("chat_reactions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.RealtimeCall", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("ProviderName") + .HasColumnType("text") + .HasColumnName("provider_name"); + + b.Property("RoomId") + .HasColumnType("uuid") + .HasColumnName("room_id"); + + b.Property("SenderId") + .HasColumnType("uuid") + .HasColumnName("sender_id"); + + b.Property("SessionId") + .HasColumnType("text") + .HasColumnName("session_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpstreamConfigJson") + .HasColumnType("jsonb") + .HasColumnName("upstream"); + + b.HasKey("Id") + .HasName("pk_chat_realtime_call"); + + b.HasIndex("RoomId") + .HasDatabaseName("ix_chat_realtime_call_room_id"); + + b.HasIndex("SenderId") + .HasDatabaseName("ix_chat_realtime_call_sender_id"); + + b.ToTable("chat_realtime_call", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomApp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("slug"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("VerifiedAs") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("verified_as"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_custom_apps"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_custom_apps_publisher_id"); + + b.ToTable("custom_apps", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomAppSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AppId") + .HasColumnType("uuid") + .HasColumnName("app_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Remarks") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("remarks"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("secret"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_custom_app_secrets"); + + b.HasIndex("AppId") + .HasDatabaseName("ix_custom_app_secrets_app_id"); + + b.ToTable("custom_app_secrets", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("key"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_permission_groups"); + + b.ToTable("permission_groups", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroupMember", b => + { + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("Actor") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("actor"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("GroupId", "Actor") + .HasName("pk_permission_group_members"); + + b.ToTable("permission_group_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("actor"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("Area") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("area"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("key"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_permission_nodes"); + + b.HasIndex("GroupId") + .HasDatabaseName("ix_permission_nodes_group_id"); + + b.HasIndex("Key", "Area", "Actor") + .HasDatabaseName("ix_permission_nodes_key_area_actor"); + + b.ToTable("permission_nodes", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property>("Attachments") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("attachments"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("Downvotes") + .HasColumnType("integer") + .HasColumnName("downvotes"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("edited_at"); + + b.Property("ForwardedPostId") + .HasColumnType("uuid") + .HasColumnName("forwarded_post_id"); + + b.Property("Language") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("language"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("RepliedPostId") + .HasColumnType("uuid") + .HasColumnName("replied_post_id"); + + b.Property("SearchVector") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("tsvector") + .HasColumnName("search_vector") + .HasAnnotation("Npgsql:TsVectorConfig", "simple") + .HasAnnotation("Npgsql:TsVectorProperties", new[] { "Title", "Description", "Content" }); + + b.Property("Title") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("title"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Upvotes") + .HasColumnType("integer") + .HasColumnName("upvotes"); + + b.Property("ViewsTotal") + .HasColumnType("integer") + .HasColumnName("views_total"); + + b.Property("ViewsUnique") + .HasColumnType("integer") + .HasColumnName("views_unique"); + + b.Property("Visibility") + .HasColumnType("integer") + .HasColumnName("visibility"); + + b.HasKey("Id") + .HasName("pk_posts"); + + b.HasIndex("ForwardedPostId") + .HasDatabaseName("ix_posts_forwarded_post_id"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_posts_publisher_id"); + + b.HasIndex("RepliedPostId") + .HasDatabaseName("ix_posts_replied_post_id"); + + b.HasIndex("SearchVector") + .HasDatabaseName("ix_posts_search_vector"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "GIN"); + + b.ToTable("posts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_post_categories"); + + b.ToTable("post_categories", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_post_collections"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_post_collections_publisher_id"); + + b.ToTable("post_collections", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Attitude") + .HasColumnType("integer") + .HasColumnName("attitude"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("PostId") + .HasColumnType("uuid") + .HasColumnName("post_id"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("symbol"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_post_reactions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_post_reactions_account_id"); + + b.HasIndex("PostId") + .HasDatabaseName("ix_post_reactions_post_id"); + + b.ToTable("post_reactions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_post_tags"); + + b.ToTable("post_tags", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.Publisher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("bio"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Nick") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("nick"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_publishers"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_publishers_account_id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_publishers_name"); + + b.HasIndex("RealmId") + .HasDatabaseName("ix_publishers_realm_id"); + + b.ToTable("publishers", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Flag") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("flag"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_publisher_features"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_publisher_features_publisher_id"); + + b.ToTable("publisher_features", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherMember", b => + { + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_at"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("PublisherId", "AccountId") + .HasName("pk_publisher_members"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_publisher_members_account_id"); + + b.ToTable("publisher_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Tier") + .HasColumnType("integer") + .HasColumnName("tier"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_publisher_subscriptions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_publisher_subscriptions_account_id"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_publisher_subscriptions_publisher_id"); + + b.ToTable("publisher_subscriptions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("IsCommunity") + .HasColumnType("boolean") + .HasColumnName("is_community"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("VerifiedAs") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("verified_as"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_realms"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_realms_account_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_realms_slug"); + + b.ToTable("realms", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.RealmMember", b => + { + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_at"); + + b.Property("LeaveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("leave_at"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("RealmId", "AccountId") + .HasName("pk_realm_members"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_realm_members_account_id"); + + b.ToTable("realm_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Sticker.Sticker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Image") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("image"); + + b.Property("ImageId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("image_id"); + + b.Property("PackId") + .HasColumnType("uuid") + .HasColumnName("pack_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_stickers"); + + b.HasIndex("PackId") + .HasDatabaseName("ix_stickers_pack_id"); + + b.HasIndex("Slug") + .HasDatabaseName("ix_stickers_slug"); + + b.ToTable("stickers", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Sticker.StickerPack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("Prefix") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("prefix"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_sticker_packs"); + + b.HasIndex("Prefix") + .IsUnique() + .HasDatabaseName("ix_sticker_packs_prefix"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_sticker_packs_publisher_id"); + + b.ToTable("sticker_packs", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property>("FileMeta") + .HasColumnType("jsonb") + .HasColumnName("file_meta"); + + b.Property("HasCompression") + .HasColumnType("boolean") + .HasColumnName("has_compression"); + + b.Property("Hash") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("hash"); + + b.Property("MimeType") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("mime_type"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property>("SensitiveMarks") + .HasColumnType("jsonb") + .HasColumnName("sensitive_marks"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("StorageId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("storage_id"); + + b.Property("StorageUrl") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("storage_url"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("uploaded_at"); + + b.Property("UploadedTo") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("uploaded_to"); + + b.Property>("UserMeta") + .HasColumnType("jsonb") + .HasColumnName("user_meta"); + + b.HasKey("Id") + .HasName("pk_files"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_files_account_id"); + + b.ToTable("files", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFileReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("FileId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("file_id"); + + b.Property("ResourceId") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("resource_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Usage") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("usage"); + + b.HasKey("Id") + .HasName("pk_file_references"); + + b.HasIndex("FileId") + .HasDatabaseName("ix_file_references_file_id"); + + b.ToTable("file_references", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("IssuerAppId") + .HasColumnType("uuid") + .HasColumnName("issuer_app_id"); + + b.Property("PayeeWalletId") + .HasColumnType("uuid") + .HasColumnName("payee_wallet_id"); + + b.Property("Remarks") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("remarks"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TransactionId") + .HasColumnType("uuid") + .HasColumnName("transaction_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_payment_orders"); + + b.HasIndex("IssuerAppId") + .HasDatabaseName("ix_payment_orders_issuer_app_id"); + + b.HasIndex("PayeeWalletId") + .HasDatabaseName("ix_payment_orders_payee_wallet_id"); + + b.HasIndex("TransactionId") + .HasDatabaseName("ix_payment_orders_transaction_id"); + + b.ToTable("payment_orders", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("PayeeWalletId") + .HasColumnType("uuid") + .HasColumnName("payee_wallet_id"); + + b.Property("PayerWalletId") + .HasColumnType("uuid") + .HasColumnName("payer_wallet_id"); + + b.Property("Remarks") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("remarks"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_payment_transactions"); + + b.HasIndex("PayeeWalletId") + .HasDatabaseName("ix_payment_transactions_payee_wallet_id"); + + b.HasIndex("PayerWalletId") + .HasDatabaseName("ix_payment_transactions_payer_wallet_id"); + + b.ToTable("payment_transactions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Wallet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallets"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_wallets_account_id"); + + b.ToTable("wallets", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.WalletPocket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("WalletId") + .HasColumnType("uuid") + .HasColumnName("wallet_id"); + + b.HasKey("Id") + .HasName("pk_wallet_pockets"); + + b.HasIndex("WalletId") + .HasDatabaseName("ix_wallet_pockets_wallet_id"); + + b.ToTable("wallet_pockets", (string)null); + }); + + modelBuilder.Entity("PostPostCategory", b => + { + b.Property("CategoriesId") + .HasColumnType("uuid") + .HasColumnName("categories_id"); + + b.Property("PostsId") + .HasColumnType("uuid") + .HasColumnName("posts_id"); + + b.HasKey("CategoriesId", "PostsId") + .HasName("pk_post_category_links"); + + b.HasIndex("PostsId") + .HasDatabaseName("ix_post_category_links_posts_id"); + + b.ToTable("post_category_links", (string)null); + }); + + modelBuilder.Entity("PostPostCollection", b => + { + b.Property("CollectionsId") + .HasColumnType("uuid") + .HasColumnName("collections_id"); + + b.Property("PostsId") + .HasColumnType("uuid") + .HasColumnName("posts_id"); + + b.HasKey("CollectionsId", "PostsId") + .HasName("pk_post_collection_links"); + + b.HasIndex("PostsId") + .HasDatabaseName("ix_post_collection_links_posts_id"); + + b.ToTable("post_collection_links", (string)null); + }); + + modelBuilder.Entity("PostPostTag", b => + { + b.Property("PostsId") + .HasColumnType("uuid") + .HasColumnName("posts_id"); + + b.Property("TagsId") + .HasColumnType("uuid") + .HasColumnName("tags_id"); + + b.HasKey("PostsId", "TagsId") + .HasName("pk_post_tag_links"); + + b.HasIndex("TagsId") + .HasDatabaseName("ix_post_tag_links_tags_id"); + + b.ToTable("post_tag_links", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountAuthFactor", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("AuthFactors") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_auth_factors_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("Contacts") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_contacts_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.ActionLog", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_action_logs_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Auth.Session", "Session") + .WithMany() + .HasForeignKey("SessionId") + .HasConstraintName("fk_action_logs_auth_sessions_session_id"); + + b.Navigation("Account"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Badge", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("Badges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_badges_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.CheckInResult", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_check_in_results_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.MagicSpell", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .HasConstraintName("fk_magic_spells_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Notification", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notifications_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.NotificationPushSubscription", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notification_push_subscriptions_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Profile", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithOne("Profile") + .HasForeignKey("DysonNetwork.Sphere.Account.Profile", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_profiles_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Relationship", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("OutgoingRelationships") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Account.Account", "Related") + .WithMany("IncomingRelationships") + .HasForeignKey("RelatedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_related_id"); + + b.Navigation("Account"); + + b.Navigation("Related"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Status", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_statuses_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Activity.Activity", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_activities_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Auth.Challenge", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("Challenges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_challenges_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Auth.Session", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("Sessions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_sessions_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Auth.Challenge", "Challenge") + .WithMany() + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); + + b.Navigation("Account"); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.ChatMember", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_members_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.ChatRoom", "ChatRoom") + .WithMany("Members") + .HasForeignKey("ChatRoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_members_chat_rooms_chat_room_id"); + + b.Navigation("Account"); + + b.Navigation("ChatRoom"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.ChatRoom", b => + { + b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm") + .WithMany("ChatRooms") + .HasForeignKey("RealmId") + .HasConstraintName("fk_chat_rooms_realms_realm_id"); + + b.Navigation("Realm"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.Message", b => + { + b.HasOne("DysonNetwork.Sphere.Chat.ChatRoom", "ChatRoom") + .WithMany() + .HasForeignKey("ChatRoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_messages_chat_rooms_chat_room_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.Message", "ForwardedMessage") + .WithMany() + .HasForeignKey("ForwardedMessageId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_chat_messages_chat_messages_forwarded_message_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.Message", "RepliedMessage") + .WithMany() + .HasForeignKey("RepliedMessageId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_chat_messages_chat_messages_replied_message_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.ChatMember", "Sender") + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_messages_chat_members_sender_id"); + + b.Navigation("ChatRoom"); + + b.Navigation("ForwardedMessage"); + + b.Navigation("RepliedMessage"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.MessageReaction", b => + { + b.HasOne("DysonNetwork.Sphere.Chat.Message", "Message") + .WithMany("Reactions") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_reactions_chat_messages_message_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.ChatMember", "Sender") + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_reactions_chat_members_sender_id"); + + b.Navigation("Message"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.RealtimeCall", b => + { + b.HasOne("DysonNetwork.Sphere.Chat.ChatRoom", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_realtime_call_chat_rooms_room_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.ChatMember", "Sender") + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_realtime_call_chat_members_sender_id"); + + b.Navigation("Room"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomApp", b => + { + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Developer") + .WithMany() + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_custom_apps_publishers_publisher_id"); + + b.Navigation("Developer"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomAppSecret", b => + { + b.HasOne("DysonNetwork.Sphere.Developer.CustomApp", "App") + .WithMany() + .HasForeignKey("AppId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id"); + + b.Navigation("App"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroupMember", b => + { + b.HasOne("DysonNetwork.Sphere.Permission.PermissionGroup", "Group") + .WithMany("Members") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_permission_group_members_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionNode", b => + { + b.HasOne("DysonNetwork.Sphere.Permission.PermissionGroup", "Group") + .WithMany("Nodes") + .HasForeignKey("GroupId") + .HasConstraintName("fk_permission_nodes_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b => + { + b.HasOne("DysonNetwork.Sphere.Post.Post", "ForwardedPost") + .WithMany() + .HasForeignKey("ForwardedPostId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_posts_posts_forwarded_post_id"); + + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany("Posts") + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_posts_publishers_publisher_id"); + + b.HasOne("DysonNetwork.Sphere.Post.Post", "RepliedPost") + .WithMany() + .HasForeignKey("RepliedPostId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_posts_posts_replied_post_id"); + + b.Navigation("ForwardedPost"); + + b.Navigation("Publisher"); + + b.Navigation("RepliedPost"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostCollection", b => + { + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany("Collections") + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_collections_publishers_publisher_id"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostReaction", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_reactions_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Post.Post", "Post") + .WithMany("Reactions") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_reactions_posts_post_id"); + + b.Navigation("Account"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.Publisher", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .HasConstraintName("fk_publishers_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm") + .WithMany() + .HasForeignKey("RealmId") + .HasConstraintName("fk_publishers_realms_realm_id"); + + b.Navigation("Account"); + + b.Navigation("Realm"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherFeature", b => + { + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany() + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_publisher_features_publishers_publisher_id"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherMember", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_publisher_members_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany("Members") + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_publisher_members_publishers_publisher_id"); + + b.Navigation("Account"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherSubscription", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_publisher_subscriptions_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany("Subscriptions") + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_publisher_subscriptions_publishers_publisher_id"); + + b.Navigation("Account"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realms_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.RealmMember", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realm_members_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm") + .WithMany("Members") + .HasForeignKey("RealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realm_members_realms_realm_id"); + + b.Navigation("Account"); + + b.Navigation("Realm"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Sticker.Sticker", b => + { + b.HasOne("DysonNetwork.Sphere.Sticker.StickerPack", "Pack") + .WithMany() + .HasForeignKey("PackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_stickers_sticker_packs_pack_id"); + + b.Navigation("Pack"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Sticker.StickerPack", b => + { + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany() + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_sticker_packs_publishers_publisher_id"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_files_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFileReference", b => + { + b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_file_references_files_file_id"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Order", b => + { + b.HasOne("DysonNetwork.Sphere.Developer.CustomApp", "IssuerApp") + .WithMany() + .HasForeignKey("IssuerAppId") + .HasConstraintName("fk_payment_orders_custom_apps_issuer_app_id"); + + b.HasOne("DysonNetwork.Sphere.Wallet.Wallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_payment_orders_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Sphere.Wallet.Transaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId") + .HasConstraintName("fk_payment_orders_payment_transactions_transaction_id"); + + b.Navigation("IssuerApp"); + + b.Navigation("PayeeWallet"); + + b.Navigation("Transaction"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Transaction", b => + { + b.HasOne("DysonNetwork.Sphere.Wallet.Wallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Sphere.Wallet.Wallet", "PayerWallet") + .WithMany() + .HasForeignKey("PayerWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payer_wallet_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("PayerWallet"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Wallet", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallets_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.WalletPocket", b => + { + b.HasOne("DysonNetwork.Sphere.Wallet.Wallet", "Wallet") + .WithMany("Pockets") + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_pockets_wallets_wallet_id"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("PostPostCategory", b => + { + b.HasOne("DysonNetwork.Sphere.Post.PostCategory", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_category_links_post_categories_categories_id"); + + b.HasOne("DysonNetwork.Sphere.Post.Post", null) + .WithMany() + .HasForeignKey("PostsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_category_links_posts_posts_id"); + }); + + modelBuilder.Entity("PostPostCollection", b => + { + b.HasOne("DysonNetwork.Sphere.Post.PostCollection", null) + .WithMany() + .HasForeignKey("CollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_collection_links_post_collections_collections_id"); + + b.HasOne("DysonNetwork.Sphere.Post.Post", null) + .WithMany() + .HasForeignKey("PostsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_collection_links_posts_posts_id"); + }); + + modelBuilder.Entity("PostPostTag", b => + { + b.HasOne("DysonNetwork.Sphere.Post.Post", null) + .WithMany() + .HasForeignKey("PostsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_tag_links_posts_posts_id"); + + b.HasOne("DysonNetwork.Sphere.Post.PostTag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_tag_links_post_tags_tags_id"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b => + { + b.Navigation("AuthFactors"); + + b.Navigation("Badges"); + + b.Navigation("Challenges"); + + b.Navigation("Contacts"); + + b.Navigation("IncomingRelationships"); + + b.Navigation("OutgoingRelationships"); + + b.Navigation("Profile") + .IsRequired(); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.ChatRoom", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.Message", b => + { + b.Navigation("Reactions"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroup", b => + { + b.Navigation("Members"); + + b.Navigation("Nodes"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b => + { + b.Navigation("Reactions"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.Publisher", b => + { + b.Navigation("Collections"); + + b.Navigation("Members"); + + b.Navigation("Posts"); + + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b => + { + b.Navigation("ChatRooms"); + + b.Navigation("Members"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Wallet", b => + { + b.Navigation("Pockets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DysonNetwork.Sphere/Migrations/20250601111032_RefactorCloudFileReference.cs b/DysonNetwork.Sphere/Migrations/20250601111032_RefactorCloudFileReference.cs new file mode 100644 index 0000000..1e124db --- /dev/null +++ b/DysonNetwork.Sphere/Migrations/20250601111032_RefactorCloudFileReference.cs @@ -0,0 +1,557 @@ +using System; +using System.Collections.Generic; +using DysonNetwork.Sphere.Storage; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace DysonNetwork.Sphere.Migrations +{ + /// + public partial class RefactorCloudFileReference : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_account_profiles_files_background_id", + table: "account_profiles"); + + migrationBuilder.DropForeignKey( + name: "fk_account_profiles_files_picture_id", + table: "account_profiles"); + + migrationBuilder.DropForeignKey( + name: "fk_chat_rooms_files_background_id", + table: "chat_rooms"); + + migrationBuilder.DropForeignKey( + name: "fk_chat_rooms_files_picture_id", + table: "chat_rooms"); + + migrationBuilder.DropForeignKey( + name: "fk_files_chat_messages_message_id", + table: "files"); + + migrationBuilder.DropForeignKey( + name: "fk_files_posts_post_id", + table: "files"); + + migrationBuilder.DropForeignKey( + name: "fk_posts_posts_threaded_post_id", + table: "posts"); + + migrationBuilder.DropForeignKey( + name: "fk_publishers_files_background_id", + table: "publishers"); + + migrationBuilder.DropForeignKey( + name: "fk_publishers_files_picture_id", + table: "publishers"); + + migrationBuilder.DropForeignKey( + name: "fk_realms_files_background_id", + table: "realms"); + + migrationBuilder.DropForeignKey( + name: "fk_realms_files_picture_id", + table: "realms"); + + migrationBuilder.DropForeignKey( + name: "fk_stickers_files_image_id", + table: "stickers"); + + migrationBuilder.DropIndex( + name: "ix_stickers_image_id", + table: "stickers"); + + migrationBuilder.DropIndex( + name: "ix_realms_background_id", + table: "realms"); + + migrationBuilder.DropIndex( + name: "ix_realms_picture_id", + table: "realms"); + + migrationBuilder.DropIndex( + name: "ix_publishers_background_id", + table: "publishers"); + + migrationBuilder.DropIndex( + name: "ix_publishers_picture_id", + table: "publishers"); + + migrationBuilder.DropIndex( + name: "ix_posts_threaded_post_id", + table: "posts"); + + migrationBuilder.DropIndex( + name: "ix_files_message_id", + table: "files"); + + migrationBuilder.DropIndex( + name: "ix_files_post_id", + table: "files"); + + migrationBuilder.DropIndex( + name: "ix_chat_rooms_background_id", + table: "chat_rooms"); + + migrationBuilder.DropIndex( + name: "ix_chat_rooms_picture_id", + table: "chat_rooms"); + + migrationBuilder.DropIndex( + name: "ix_account_profiles_background_id", + table: "account_profiles"); + + migrationBuilder.DropIndex( + name: "ix_account_profiles_picture_id", + table: "account_profiles"); + + migrationBuilder.DropColumn( + name: "background_id", + table: "realms"); + + migrationBuilder.DropColumn( + name: "picture_id", + table: "realms"); + + migrationBuilder.DropColumn( + name: "background_id", + table: "publishers"); + + migrationBuilder.DropColumn( + name: "picture_id", + table: "publishers"); + + migrationBuilder.DropColumn( + name: "threaded_post_id", + table: "posts"); + + // TODO Move these following changes to next migrations after migrated all the attachments data + // migrationBuilder.DropColumn( + // name: "expired_at", + // table: "files"); + // + // migrationBuilder.DropColumn( + // name: "message_id", + // table: "files"); + // + // migrationBuilder.DropColumn( + // name: "post_id", + // table: "files"); + // + // migrationBuilder.DropColumn( + // name: "usage", + // table: "files"); + // + // migrationBuilder.DropColumn( + // name: "used_count", + // table: "files"); + // + // migrationBuilder.DropColumn( + // name: "background_id", + // table: "chat_rooms"); + // + // migrationBuilder.DropColumn( + // name: "picture_id", + // table: "chat_rooms"); + // + // migrationBuilder.DropColumn( + // name: "background_id", + // table: "account_profiles"); + // + // migrationBuilder.DropColumn( + // name: "picture_id", + // table: "account_profiles"); + + migrationBuilder.AddColumn( + name: "image", + table: "stickers", + type: "jsonb", + nullable: false); + + migrationBuilder.AddColumn( + name: "background", + table: "realms", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn( + name: "picture", + table: "realms", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn( + name: "background", + table: "publishers", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn( + name: "picture", + table: "publishers", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn>( + name: "attachments", + table: "posts", + type: "jsonb", + nullable: false); + + migrationBuilder.AddColumn( + name: "background", + table: "chat_rooms", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn( + name: "picture", + table: "chat_rooms", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn>( + name: "attachments", + table: "chat_messages", + type: "jsonb", + nullable: false); + + migrationBuilder.AddColumn( + name: "background", + table: "account_profiles", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn( + name: "picture", + table: "account_profiles", + type: "jsonb", + nullable: true); + + migrationBuilder.CreateTable( + name: "file_references", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + file_id = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + usage = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + resource_id = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + expired_at = table.Column(type: "timestamp with time zone", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_file_references", x => x.id); + table.ForeignKey( + name: "fk_file_references_files_file_id", + column: x => x.file_id, + principalTable: "files", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_file_references_file_id", + table: "file_references", + column: "file_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "file_references"); + + migrationBuilder.DropColumn( + name: "image", + table: "stickers"); + + migrationBuilder.DropColumn( + name: "background", + table: "realms"); + + migrationBuilder.DropColumn( + name: "picture", + table: "realms"); + + migrationBuilder.DropColumn( + name: "background", + table: "publishers"); + + migrationBuilder.DropColumn( + name: "picture", + table: "publishers"); + + migrationBuilder.DropColumn( + name: "attachments", + table: "posts"); + + migrationBuilder.DropColumn( + name: "background", + table: "chat_rooms"); + + migrationBuilder.DropColumn( + name: "picture", + table: "chat_rooms"); + + migrationBuilder.DropColumn( + name: "attachments", + table: "chat_messages"); + + migrationBuilder.DropColumn( + name: "background", + table: "account_profiles"); + + migrationBuilder.DropColumn( + name: "picture", + table: "account_profiles"); + + migrationBuilder.AddColumn( + name: "background_id", + table: "realms", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.AddColumn( + name: "picture_id", + table: "realms", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.AddColumn( + name: "background_id", + table: "publishers", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.AddColumn( + name: "picture_id", + table: "publishers", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.AddColumn( + name: "threaded_post_id", + table: "posts", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "expired_at", + table: "files", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "message_id", + table: "files", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "post_id", + table: "files", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "usage", + table: "files", + type: "character varying(1024)", + maxLength: 1024, + nullable: true); + + migrationBuilder.AddColumn( + name: "used_count", + table: "files", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "background_id", + table: "chat_rooms", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.AddColumn( + name: "picture_id", + table: "chat_rooms", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.AddColumn( + name: "background_id", + table: "account_profiles", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.AddColumn( + name: "picture_id", + table: "account_profiles", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.CreateIndex( + name: "ix_stickers_image_id", + table: "stickers", + column: "image_id"); + + migrationBuilder.CreateIndex( + name: "ix_realms_background_id", + table: "realms", + column: "background_id"); + + migrationBuilder.CreateIndex( + name: "ix_realms_picture_id", + table: "realms", + column: "picture_id"); + + migrationBuilder.CreateIndex( + name: "ix_publishers_background_id", + table: "publishers", + column: "background_id"); + + migrationBuilder.CreateIndex( + name: "ix_publishers_picture_id", + table: "publishers", + column: "picture_id"); + + migrationBuilder.CreateIndex( + name: "ix_posts_threaded_post_id", + table: "posts", + column: "threaded_post_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_files_message_id", + table: "files", + column: "message_id"); + + migrationBuilder.CreateIndex( + name: "ix_files_post_id", + table: "files", + column: "post_id"); + + migrationBuilder.CreateIndex( + name: "ix_chat_rooms_background_id", + table: "chat_rooms", + column: "background_id"); + + migrationBuilder.CreateIndex( + name: "ix_chat_rooms_picture_id", + table: "chat_rooms", + column: "picture_id"); + + migrationBuilder.CreateIndex( + name: "ix_account_profiles_background_id", + table: "account_profiles", + column: "background_id"); + + migrationBuilder.CreateIndex( + name: "ix_account_profiles_picture_id", + table: "account_profiles", + column: "picture_id"); + + migrationBuilder.AddForeignKey( + name: "fk_account_profiles_files_background_id", + table: "account_profiles", + column: "background_id", + principalTable: "files", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_account_profiles_files_picture_id", + table: "account_profiles", + column: "picture_id", + principalTable: "files", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_chat_rooms_files_background_id", + table: "chat_rooms", + column: "background_id", + principalTable: "files", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_chat_rooms_files_picture_id", + table: "chat_rooms", + column: "picture_id", + principalTable: "files", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_files_chat_messages_message_id", + table: "files", + column: "message_id", + principalTable: "chat_messages", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_files_posts_post_id", + table: "files", + column: "post_id", + principalTable: "posts", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_posts_posts_threaded_post_id", + table: "posts", + column: "threaded_post_id", + principalTable: "posts", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_publishers_files_background_id", + table: "publishers", + column: "background_id", + principalTable: "files", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_publishers_files_picture_id", + table: "publishers", + column: "picture_id", + principalTable: "files", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_realms_files_background_id", + table: "realms", + column: "background_id", + principalTable: "files", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_realms_files_picture_id", + table: "realms", + column: "picture_id", + principalTable: "files", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_stickers_files_image_id", + table: "stickers", + column: "image_id", + principalTable: "files", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/DysonNetwork.Sphere/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Sphere/Migrations/AppDatabaseModelSnapshot.cs index 5f41c90..5d78e89 100644 --- a/DysonNetwork.Sphere/Migrations/AppDatabaseModelSnapshot.cs +++ b/DysonNetwork.Sphere/Migrations/AppDatabaseModelSnapshot.cs @@ -533,10 +533,9 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("uuid") .HasColumnName("account_id"); - b.Property("BackgroundId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("background_id"); + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); b.Property("Bio") .HasMaxLength(4096) @@ -583,10 +582,9 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("character varying(256)") .HasColumnName("middle_name"); - b.Property("PictureId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("picture_id"); + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); b.Property("Pronouns") .HasMaxLength(1024) @@ -604,12 +602,6 @@ namespace DysonNetwork.Sphere.Migrations .IsUnique() .HasDatabaseName("ix_account_profiles_account_id"); - b.HasIndex("BackgroundId") - .HasDatabaseName("ix_account_profiles_background_id"); - - b.HasIndex("PictureId") - .HasDatabaseName("ix_account_profiles_picture_id"); - b.ToTable("account_profiles", (string)null); }); @@ -985,10 +977,9 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("uuid") .HasColumnName("id"); - b.Property("BackgroundId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("background_id"); + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") @@ -1016,10 +1007,9 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("character varying(1024)") .HasColumnName("name"); - b.Property("PictureId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("picture_id"); + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); b.Property("RealmId") .HasColumnType("uuid") @@ -1036,12 +1026,6 @@ namespace DysonNetwork.Sphere.Migrations b.HasKey("Id") .HasName("pk_chat_rooms"); - b.HasIndex("BackgroundId") - .HasDatabaseName("ix_chat_rooms_background_id"); - - b.HasIndex("PictureId") - .HasDatabaseName("ix_chat_rooms_picture_id"); - b.HasIndex("RealmId") .HasDatabaseName("ix_chat_rooms_realm_id"); @@ -1055,6 +1039,11 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("uuid") .HasColumnName("id"); + b.Property>("Attachments") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("attachments"); + b.Property("ChatRoomId") .HasColumnType("uuid") .HasColumnName("chat_room_id"); @@ -1479,6 +1468,11 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("uuid") .HasColumnName("id"); + b.Property>("Attachments") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("attachments"); + b.Property("Content") .HasColumnType("text") .HasColumnName("content"); @@ -1537,10 +1531,6 @@ namespace DysonNetwork.Sphere.Migrations .HasAnnotation("Npgsql:TsVectorConfig", "simple") .HasAnnotation("Npgsql:TsVectorProperties", new[] { "Title", "Description", "Content" }); - b.Property("ThreadedPostId") - .HasColumnType("uuid") - .HasColumnName("threaded_post_id"); - b.Property("Title") .HasMaxLength(1024) .HasColumnType("character varying(1024)") @@ -1587,10 +1577,6 @@ namespace DysonNetwork.Sphere.Migrations NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "GIN"); - b.HasIndex("ThreadedPostId") - .IsUnique() - .HasDatabaseName("ix_posts_threaded_post_id"); - b.ToTable("posts", (string)null); }); @@ -1774,10 +1760,9 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("uuid") .HasColumnName("account_id"); - b.Property("BackgroundId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("background_id"); + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); b.Property("Bio") .HasMaxLength(4096) @@ -1804,10 +1789,9 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("character varying(256)") .HasColumnName("nick"); - b.Property("PictureId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("picture_id"); + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); b.Property("RealmId") .HasColumnType("uuid") @@ -1827,16 +1811,10 @@ namespace DysonNetwork.Sphere.Migrations b.HasIndex("AccountId") .HasDatabaseName("ix_publishers_account_id"); - b.HasIndex("BackgroundId") - .HasDatabaseName("ix_publishers_background_id"); - b.HasIndex("Name") .IsUnique() .HasDatabaseName("ix_publishers_name"); - b.HasIndex("PictureId") - .HasDatabaseName("ix_publishers_picture_id"); - b.HasIndex("RealmId") .HasDatabaseName("ix_publishers_realm_id"); @@ -1982,10 +1960,9 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("uuid") .HasColumnName("account_id"); - b.Property("BackgroundId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("background_id"); + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") @@ -2015,10 +1992,9 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("character varying(1024)") .HasColumnName("name"); - b.Property("PictureId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("picture_id"); + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); b.Property("Slug") .IsRequired() @@ -2045,12 +2021,6 @@ namespace DysonNetwork.Sphere.Migrations b.HasIndex("AccountId") .HasDatabaseName("ix_realms_account_id"); - b.HasIndex("BackgroundId") - .HasDatabaseName("ix_realms_background_id"); - - b.HasIndex("PictureId") - .HasDatabaseName("ix_realms_picture_id"); - b.HasIndex("Slug") .IsUnique() .HasDatabaseName("ix_realms_slug"); @@ -2116,6 +2086,11 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("deleted_at"); + b.Property("Image") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("image"); + b.Property("ImageId") .IsRequired() .HasMaxLength(32) @@ -2139,9 +2114,6 @@ namespace DysonNetwork.Sphere.Migrations b.HasKey("Id") .HasName("pk_stickers"); - b.HasIndex("ImageId") - .HasDatabaseName("ix_stickers_image_id"); - b.HasIndex("PackId") .HasDatabaseName("ix_stickers_pack_id"); @@ -2229,10 +2201,6 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("character varying(4096)") .HasColumnName("description"); - b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expired_at"); - b.Property>("FileMeta") .HasColumnType("jsonb") .HasColumnName("file_meta"); @@ -2246,10 +2214,6 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("character varying(256)") .HasColumnName("hash"); - b.Property("MessageId") - .HasColumnType("uuid") - .HasColumnName("message_id"); - b.Property("MimeType") .HasMaxLength(256) .HasColumnType("character varying(256)") @@ -2261,10 +2225,6 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("character varying(1024)") .HasColumnName("name"); - b.Property("PostId") - .HasColumnType("uuid") - .HasColumnName("post_id"); - b.Property>("SensitiveMarks") .HasColumnType("jsonb") .HasColumnName("sensitive_marks"); @@ -2296,15 +2256,6 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("character varying(128)") .HasColumnName("uploaded_to"); - b.Property("Usage") - .HasMaxLength(1024) - .HasColumnType("character varying(1024)") - .HasColumnName("usage"); - - b.Property("UsedCount") - .HasColumnType("integer") - .HasColumnName("used_count"); - b.Property>("UserMeta") .HasColumnType("jsonb") .HasColumnName("user_meta"); @@ -2315,15 +2266,59 @@ namespace DysonNetwork.Sphere.Migrations b.HasIndex("AccountId") .HasDatabaseName("ix_files_account_id"); - b.HasIndex("MessageId") - .HasDatabaseName("ix_files_message_id"); - - b.HasIndex("PostId") - .HasDatabaseName("ix_files_post_id"); - b.ToTable("files", (string)null); }); + modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFileReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("FileId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("file_id"); + + b.Property("ResourceId") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("resource_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Usage") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("usage"); + + b.HasKey("Id") + .HasName("pk_file_references"); + + b.HasIndex("FileId") + .HasDatabaseName("ix_file_references_file_id"); + + b.ToTable("file_references", (string)null); + }); + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Order", b => { b.Property("Id") @@ -2692,21 +2687,7 @@ namespace DysonNetwork.Sphere.Migrations .IsRequired() .HasConstraintName("fk_account_profiles_accounts_account_id"); - b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Background") - .WithMany() - .HasForeignKey("BackgroundId") - .HasConstraintName("fk_account_profiles_files_background_id"); - - b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Picture") - .WithMany() - .HasForeignKey("PictureId") - .HasConstraintName("fk_account_profiles_files_picture_id"); - b.Navigation("Account"); - - b.Navigation("Background"); - - b.Navigation("Picture"); }); modelBuilder.Entity("DysonNetwork.Sphere.Account.Relationship", b => @@ -2810,25 +2791,11 @@ namespace DysonNetwork.Sphere.Migrations modelBuilder.Entity("DysonNetwork.Sphere.Chat.ChatRoom", b => { - b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Background") - .WithMany() - .HasForeignKey("BackgroundId") - .HasConstraintName("fk_chat_rooms_files_background_id"); - - b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Picture") - .WithMany() - .HasForeignKey("PictureId") - .HasConstraintName("fk_chat_rooms_files_picture_id"); - b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm") .WithMany("ChatRooms") .HasForeignKey("RealmId") .HasConstraintName("fk_chat_rooms_realms_realm_id"); - b.Navigation("Background"); - - b.Navigation("Picture"); - b.Navigation("Realm"); }); @@ -2978,18 +2945,11 @@ namespace DysonNetwork.Sphere.Migrations .OnDelete(DeleteBehavior.Restrict) .HasConstraintName("fk_posts_posts_replied_post_id"); - b.HasOne("DysonNetwork.Sphere.Post.Post", "ThreadedPost") - .WithOne() - .HasForeignKey("DysonNetwork.Sphere.Post.Post", "ThreadedPostId") - .HasConstraintName("fk_posts_posts_threaded_post_id"); - b.Navigation("ForwardedPost"); b.Navigation("Publisher"); b.Navigation("RepliedPost"); - - b.Navigation("ThreadedPost"); }); modelBuilder.Entity("DysonNetwork.Sphere.Post.PostCollection", b => @@ -3032,16 +2992,6 @@ namespace DysonNetwork.Sphere.Migrations .HasForeignKey("AccountId") .HasConstraintName("fk_publishers_accounts_account_id"); - b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Background") - .WithMany() - .HasForeignKey("BackgroundId") - .HasConstraintName("fk_publishers_files_background_id"); - - b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Picture") - .WithMany() - .HasForeignKey("PictureId") - .HasConstraintName("fk_publishers_files_picture_id"); - b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm") .WithMany() .HasForeignKey("RealmId") @@ -3049,10 +2999,6 @@ namespace DysonNetwork.Sphere.Migrations b.Navigation("Account"); - b.Navigation("Background"); - - b.Navigation("Picture"); - b.Navigation("Realm"); }); @@ -3119,21 +3065,7 @@ namespace DysonNetwork.Sphere.Migrations .IsRequired() .HasConstraintName("fk_realms_accounts_account_id"); - b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Background") - .WithMany() - .HasForeignKey("BackgroundId") - .HasConstraintName("fk_realms_files_background_id"); - - b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Picture") - .WithMany() - .HasForeignKey("PictureId") - .HasConstraintName("fk_realms_files_picture_id"); - b.Navigation("Account"); - - b.Navigation("Background"); - - b.Navigation("Picture"); }); modelBuilder.Entity("DysonNetwork.Sphere.Realm.RealmMember", b => @@ -3159,13 +3091,6 @@ namespace DysonNetwork.Sphere.Migrations modelBuilder.Entity("DysonNetwork.Sphere.Sticker.Sticker", b => { - b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Image") - .WithMany() - .HasForeignKey("ImageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_stickers_files_image_id"); - b.HasOne("DysonNetwork.Sphere.Sticker.StickerPack", "Pack") .WithMany() .HasForeignKey("PackId") @@ -3173,8 +3098,6 @@ namespace DysonNetwork.Sphere.Migrations .IsRequired() .HasConstraintName("fk_stickers_sticker_packs_pack_id"); - b.Navigation("Image"); - b.Navigation("Pack"); }); @@ -3199,19 +3122,21 @@ namespace DysonNetwork.Sphere.Migrations .IsRequired() .HasConstraintName("fk_files_accounts_account_id"); - b.HasOne("DysonNetwork.Sphere.Chat.Message", null) - .WithMany("Attachments") - .HasForeignKey("MessageId") - .HasConstraintName("fk_files_chat_messages_message_id"); - - b.HasOne("DysonNetwork.Sphere.Post.Post", null) - .WithMany("Attachments") - .HasForeignKey("PostId") - .HasConstraintName("fk_files_posts_post_id"); - b.Navigation("Account"); }); + modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFileReference", b => + { + b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_file_references_files_file_id"); + + b.Navigation("File"); + }); + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Order", b => { b.HasOne("DysonNetwork.Sphere.Developer.CustomApp", "IssuerApp") @@ -3357,8 +3282,6 @@ namespace DysonNetwork.Sphere.Migrations modelBuilder.Entity("DysonNetwork.Sphere.Chat.Message", b => { - b.Navigation("Attachments"); - b.Navigation("Reactions"); }); @@ -3371,8 +3294,6 @@ namespace DysonNetwork.Sphere.Migrations modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b => { - b.Navigation("Attachments"); - b.Navigation("Reactions"); }); diff --git a/DysonNetwork.Sphere/Post/Post.cs b/DysonNetwork.Sphere/Post/Post.cs index 7a39f47..cb0891b 100644 --- a/DysonNetwork.Sphere/Post/Post.cs +++ b/DysonNetwork.Sphere/Post/Post.cs @@ -22,7 +22,7 @@ public enum PostVisibility Private } -public class Post : ModelBase +public class Post : ModelBase, IIdentifiedResource { public Guid Id { get; set; } [MaxLength(1024)] public string? Title { get; set; } @@ -31,7 +31,7 @@ public class Post : ModelBase public Instant? EditedAt { get; set; } public Instant? PublishedAt { get; set; } public PostVisibility Visibility { get; set; } = PostVisibility.Public; - + // ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength public string? Content { get; set; } @@ -44,19 +44,17 @@ public class Post : ModelBase public int Downvotes { get; set; } [NotMapped] public Dictionary ReactionsCount { get; set; } = new(); - public Guid? ThreadedPostId { get; set; } - public Post? ThreadedPost { get; set; } public Guid? RepliedPostId { get; set; } public Post? RepliedPost { get; set; } public Guid? ForwardedPostId { get; set; } public Post? ForwardedPost { get; set; } - public ICollection Attachments { get; set; } = new List(); + [Column(TypeName = "jsonb")] public List Attachments { get; set; } = []; [JsonIgnore] public NpgsqlTsVector SearchVector { get; set; } = null!; public Guid PublisherId { get; set; } public Publisher.Publisher Publisher { get; set; } = null!; - + public ICollection Reactions { get; set; } = new List(); public ICollection Tags { get; set; } = new List(); public ICollection Categories { get; set; } = new List(); @@ -64,6 +62,8 @@ public class Post : ModelBase [JsonIgnore] public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null; [NotMapped] public bool IsTruncated = false; + + public string ResourceIdentifier => $"post/{Id}"; } public class PostTag : ModelBase diff --git a/DysonNetwork.Sphere/Post/PostController.cs b/DysonNetwork.Sphere/Post/PostController.cs index 344bfc6..7cf8cc5 100644 --- a/DysonNetwork.Sphere/Post/PostController.cs +++ b/DysonNetwork.Sphere/Post/PostController.cs @@ -41,9 +41,7 @@ public class PostController( .CountAsync(); var posts = await query .Include(e => e.RepliedPost) - .Include(e => e.ThreadedPost) .Include(e => e.ForwardedPost) - .Include(e => e.Attachments) .Include(e => e.Categories) .Include(e => e.Tags) .Where(e => e.RepliedPostId == null) @@ -107,9 +105,7 @@ public class PostController( .CountAsync(); var posts = await db.Posts .Where(e => e.RepliedPostId == id) - .Include(e => e.ThreadedPost) .Include(e => e.ForwardedPost) - .Include(e => e.Attachments) .Include(e => e.Categories) .Include(e => e.Tags) .FilterWithVisibility(currentUser, userFriends, isListing: true) @@ -351,7 +347,6 @@ public class PostController( var post = await db.Posts .Where(e => e.Id == id) .Include(e => e.Publisher) - .Include(e => e.Attachments) .FirstOrDefaultAsync(); if (post is null) return NotFound(); diff --git a/DysonNetwork.Sphere/Post/PostService.cs b/DysonNetwork.Sphere/Post/PostService.cs index 562dbda..193499f 100644 --- a/DysonNetwork.Sphere/Post/PostService.cs +++ b/DysonNetwork.Sphere/Post/PostService.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using DysonNetwork.Sphere.Account; using DysonNetwork.Sphere.Activity; using DysonNetwork.Sphere.Localization; @@ -13,6 +12,7 @@ namespace DysonNetwork.Sphere.Post; public class PostService( AppDatabase db, FileService fs, + FileReferenceService fileRefService, ActivityService act, IStringLocalizer localizer, NotificationService nty, @@ -57,7 +57,8 @@ public class PostService( if (attachments is not null) { - post.Attachments = await db.Files.Where(e => attachments.Contains(e.Id)).ToListAsync(); + post.Attachments = (await db.Files.Where(e => attachments.Contains(e.Id)).ToListAsync()) + .Select(x => x.ToReferenceObject()).ToList(); // Re-order the list to match the id list places post.Attachments = attachments .Select(id => post.Attachments.First(a => a.Id == id)) @@ -91,8 +92,20 @@ public class PostService( db.Posts.Add(post); await db.SaveChangesAsync(); - await fs.MarkUsageRangeAsync(post.Attachments, 1); - await fs.SetUsageRangeAsync(post.Attachments, PostFileUsageIdentifier); + + // Create file references for each attachment + if (post.Attachments.Any()) + { + var postResourceId = $"post:{post.Id}"; + foreach (var file in post.Attachments) + { + await fileRefService.CreateReferenceAsync( + file.Id, + PostFileUsageIdentifier, + postResourceId + ); + } + } await act.CreateNewPostActivity(user, post); @@ -131,8 +144,21 @@ public class PostService( if (attachments is not null) { - post.Attachments = (await fs.DiffAndMarkFilesAsync(attachments, post.Attachments)).current; - await fs.DiffAndSetUsageAsync(attachments, PostFileUsageIdentifier); + var postResourceId = $"post:{post.Id}"; + + // Update resource references using the new file list + await fileRefService.UpdateResourceFilesAsync( + postResourceId, + attachments, + PostFileUsageIdentifier + ); + + // Update post attachments by getting files from database + var files = await db.Files + .Where(f => attachments.Contains(f.Id)) + .ToListAsync(); + + post.Attachments = files.Select(x => x.ToReferenceObject()).ToList(); } if (tags is not null) @@ -168,9 +194,13 @@ public class PostService( public async Task DeletePostAsync(Post post) { + var postResourceId = $"post:{post.Id}"; + + // Delete all file references for this post + await fileRefService.DeleteResourceReferencesAsync(postResourceId); + db.Posts.Remove(post); await db.SaveChangesAsync(); - await fs.MarkUsageRangeAsync(post.Attachments, -1); } /// @@ -279,8 +309,7 @@ public class PostService( [ e.PublisherId, e.RepliedPost?.PublisherId, - e.ForwardedPost?.PublisherId, - e.ThreadedPost?.PublisherId + e.ForwardedPost?.PublisherId ]) .Where(e => e != null) .Distinct() @@ -300,13 +329,9 @@ public class PostService( publishers.TryGetValue(post.RepliedPost.PublisherId, out var repliedPublisher)) post.RepliedPost.Publisher = repliedPublisher; - if (post.ForwardedPost?.PublisherId != null && + if (post.ForwardedPost?.PublisherId != null && publishers.TryGetValue(post.ForwardedPost.PublisherId, out var forwardedPublisher)) post.ForwardedPost.Publisher = forwardedPublisher; - - if (post.ThreadedPost?.PublisherId != null && - publishers.TryGetValue(post.ThreadedPost.PublisherId, out var threadedPublisher)) - post.ThreadedPost.Publisher = threadedPublisher; } return posts; diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs index 8f56004..bfba11a 100644 --- a/DysonNetwork.Sphere/Program.cs +++ b/DysonNetwork.Sphere/Program.cs @@ -207,6 +207,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/DysonNetwork.Sphere/Publisher/Publisher.cs b/DysonNetwork.Sphere/Publisher/Publisher.cs index fe2a9e0..63a3450 100644 --- a/DysonNetwork.Sphere/Publisher/Publisher.cs +++ b/DysonNetwork.Sphere/Publisher/Publisher.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; using DysonNetwork.Sphere.Post; using DysonNetwork.Sphere.Storage; @@ -22,10 +23,8 @@ public class Publisher : ModelBase [MaxLength(256)] public string Nick { get; set; } = string.Empty; [MaxLength(4096)] public string? Bio { get; set; } - [MaxLength(32)] public string? PictureId { get; set; } - public CloudFile? Picture { get; set; } - [MaxLength(32)] public string? BackgroundId { get; set; } - public CloudFile? Background { get; set; } + [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; } + [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; } [JsonIgnore] public ICollection Posts { get; set; } = new List(); [JsonIgnore] public ICollection Collections { get; set; } = new List(); diff --git a/DysonNetwork.Sphere/Publisher/PublisherController.cs b/DysonNetwork.Sphere/Publisher/PublisherController.cs index d837686..4b8343c 100644 --- a/DysonNetwork.Sphere/Publisher/PublisherController.cs +++ b/DysonNetwork.Sphere/Publisher/PublisherController.cs @@ -13,7 +13,7 @@ namespace DysonNetwork.Sphere.Publisher; [ApiController] [Route("/publishers")] -public class PublisherController(AppDatabase db, PublisherService ps, FileService fs, ActionLogService als) + public class PublisherController(AppDatabase db, PublisherService ps, FileService fs, FileReferenceService fileRefService, ActionLogService als) : ControllerBase { [HttpGet("{name}")] @@ -356,20 +356,52 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic { var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync(); if (picture is null) return BadRequest("Invalid picture id."); - if (publisher.Picture is not null) await fs.MarkUsageAsync(publisher.Picture, -1); - publisher.Picture = picture; - await fs.MarkUsageAsync(picture, 1); + var publisherResourceId = $"publisher:{publisher.Id}"; + + // Remove old references for the publisher picture + if (publisher.Picture is not null) { + var oldPictureRefs = await fileRefService.GetResourceReferencesAsync(publisherResourceId, "publisher.picture"); + foreach (var oldRef in oldPictureRefs) + { + await fileRefService.DeleteReferenceAsync(oldRef.Id); + } + } + + publisher.Picture = picture.ToReferenceObject(); + + // Create a new reference + await fileRefService.CreateReferenceAsync( + picture.Id, + "publisher.picture", + publisherResourceId + ); } if (request.BackgroundId is not null) { var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync(); if (background is null) return BadRequest("Invalid background id."); - if (publisher.Background is not null) await fs.MarkUsageAsync(publisher.Background, -1); - publisher.Background = background; - await fs.MarkUsageAsync(background, 1); + var publisherResourceId = $"publisher:{publisher.Id}"; + + // Remove old references for the publisher background + if (publisher.Background is not null) { + var oldBackgroundRefs = await fileRefService.GetResourceReferencesAsync(publisherResourceId, "publisher.background"); + foreach (var oldRef in oldBackgroundRefs) + { + await fileRefService.DeleteReferenceAsync(oldRef.Id); + } + } + + publisher.Background = background.ToReferenceObject(); + + // Create a new reference + await fileRefService.CreateReferenceAsync( + background.Id, + "publisher.background", + publisherResourceId + ); } db.Update(publisher); @@ -405,10 +437,10 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic if (member.Role < PublisherMemberRole.Owner) return StatusCode(403, "You need to be the owner to delete the publisher."); - if (publisher.Picture is not null) - await fs.MarkUsageAsync(publisher.Picture, -1); - if (publisher.Background is not null) - await fs.MarkUsageAsync(publisher.Background, -1); + var publisherResourceId = $"publisher:{publisher.Id}"; + + // Delete all file references for this publisher + await fileRefService.DeleteResourceReferencesAsync(publisherResourceId); db.Publishers.Remove(publisher); await db.SaveChangesAsync(); diff --git a/DysonNetwork.Sphere/Publisher/PublisherService.cs b/DysonNetwork.Sphere/Publisher/PublisherService.cs index a43e2a3..b5d6070 100644 --- a/DysonNetwork.Sphere/Publisher/PublisherService.cs +++ b/DysonNetwork.Sphere/Publisher/PublisherService.cs @@ -6,7 +6,7 @@ using NodaTime; namespace DysonNetwork.Sphere.Publisher; -public class PublisherService(AppDatabase db, FileService fs, ICacheService cache) +public class PublisherService(AppDatabase db, FileService fs, FileReferenceService fileRefService, ICacheService cache) { public async Task CreateIndividualPublisher( Account.Account account, @@ -23,8 +23,8 @@ public class PublisherService(AppDatabase db, FileService fs, ICacheService cach Name = name ?? account.Name, Nick = nick ?? account.Nick, Bio = bio ?? account.Profile.Bio, - Picture = picture ?? account.Profile.Picture, - Background = background ?? account.Profile.Background, + Picture = picture?.ToReferenceObject() ?? account.Profile.Picture, + Background = background?.ToReferenceObject() ?? account.Profile.Background, AccountId = account.Id, Members = new List { @@ -40,8 +40,23 @@ public class PublisherService(AppDatabase db, FileService fs, ICacheService cach db.Publishers.Add(publisher); await db.SaveChangesAsync(); - if (publisher.Picture is not null) await fs.MarkUsageAsync(publisher.Picture, 1); - if (publisher.Background is not null) await fs.MarkUsageAsync(publisher.Background, 1); + var publisherResourceId = $"publisher:{publisher.Id}"; + + if (publisher.Picture is not null) { + await fileRefService.CreateReferenceAsync( + publisher.Picture.Id, + "publisher.picture", + publisherResourceId + ); + } + + if (publisher.Background is not null) { + await fileRefService.CreateReferenceAsync( + publisher.Background.Id, + "publisher.background", + publisherResourceId + ); + } return publisher; } @@ -62,8 +77,8 @@ public class PublisherService(AppDatabase db, FileService fs, ICacheService cach Name = name ?? realm.Slug, Nick = nick ?? realm.Name, Bio = bio ?? realm.Description, - Picture = picture ?? realm.Picture, - Background = background ?? realm.Background, + Picture = picture?.ToReferenceObject() ?? realm.Picture, + Background = background?.ToReferenceObject() ?? realm.Background, RealmId = realm.Id, Members = new List { @@ -79,8 +94,23 @@ public class PublisherService(AppDatabase db, FileService fs, ICacheService cach db.Publishers.Add(publisher); await db.SaveChangesAsync(); - if (publisher.Picture is not null) await fs.MarkUsageAsync(publisher.Picture, 1); - if (publisher.Background is not null) await fs.MarkUsageAsync(publisher.Background, 1); + var publisherResourceId = $"publisher:{publisher.Id}"; + + if (publisher.Picture is not null) { + await fileRefService.CreateReferenceAsync( + publisher.Picture.Id, + "publisher.picture", + publisherResourceId + ); + } + + if (publisher.Background is not null) { + await fileRefService.CreateReferenceAsync( + publisher.Background.Id, + "publisher.background", + publisherResourceId + ); + } return publisher; } diff --git a/DysonNetwork.Sphere/Realm/Realm.cs b/DysonNetwork.Sphere/Realm/Realm.cs index 1a49e51..1b68fa4 100644 --- a/DysonNetwork.Sphere/Realm/Realm.cs +++ b/DysonNetwork.Sphere/Realm/Realm.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; using DysonNetwork.Sphere.Chat; using DysonNetwork.Sphere.Storage; @@ -8,7 +9,7 @@ using NodaTime; namespace DysonNetwork.Sphere.Realm; [Index(nameof(Slug), IsUnique = true)] -public class Realm : ModelBase +public class Realm : ModelBase, IIdentifiedResource { public Guid Id { get; set; } [MaxLength(1024)] public string Slug { get; set; } = string.Empty; @@ -19,16 +20,16 @@ public class Realm : ModelBase public bool IsCommunity { get; set; } public bool IsPublic { get; set; } - [MaxLength(32)] public string? PictureId { get; set; } - public CloudFile? Picture { get; set; } - [MaxLength(32)] public string? BackgroundId { get; set; } - public CloudFile? Background { get; set; } + [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; } + [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; } [JsonIgnore] public ICollection Members { get; set; } = new List(); [JsonIgnore] public ICollection ChatRooms { get; set; } = new List(); public Guid AccountId { get; set; } [JsonIgnore] public Account.Account Account { get; set; } = null!; + + public string ResourceIdentifier => $"realm/{Id}"; } public enum RealmMemberRole diff --git a/DysonNetwork.Sphere/Realm/RealmController.cs b/DysonNetwork.Sphere/Realm/RealmController.cs index d0762c2..83b6de4 100644 --- a/DysonNetwork.Sphere/Realm/RealmController.cs +++ b/DysonNetwork.Sphere/Realm/RealmController.cs @@ -10,7 +10,14 @@ namespace DysonNetwork.Sphere.Realm; [ApiController] [Route("/realms")] -public class RealmController(AppDatabase db, RealmService rs, FileService fs, RelationshipService rels, ActionLogService als) : Controller +public class RealmController( + AppDatabase db, + RealmService rs, + FileService fs, + FileReferenceService fileRefService, + RelationshipService rels, + ActionLogService als +) : Controller { [HttpGet("{slug}")] public async Task> GetRealm(string slug) @@ -298,13 +305,13 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs, Re if (request.PictureId is not null) { - realm.Picture = await db.Files.FindAsync(request.PictureId); + realm.Picture = (await db.Files.FindAsync(request.PictureId))?.ToReferenceObject(); if (realm.Picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); } if (request.BackgroundId is not null) { - realm.Background = await db.Files.FindAsync(request.BackgroundId); + realm.Background = (await db.Files.FindAsync(request.BackgroundId))?.ToReferenceObject(); if (realm.Background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); } @@ -316,8 +323,25 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs, Re new Dictionary { { "realm_id", realm.Id } }, Request ); - if (realm.Picture is not null) await fs.MarkUsageAsync(realm.Picture, 1); - if (realm.Background is not null) await fs.MarkUsageAsync(realm.Background, 1); + var realmResourceId = $"realm:{realm.Id}"; + + if (realm.Picture is not null) + { + await fileRefService.CreateReferenceAsync( + realm.Picture.Id, + "realm.picture", + realmResourceId + ); + } + + if (realm.Background is not null) + { + await fileRefService.CreateReferenceAsync( + realm.Background.Id, + "realm.background", + realmResourceId + ); + } return Ok(realm); } @@ -357,22 +381,57 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs, Re if (request.IsPublic is not null) realm.IsPublic = request.IsPublic.Value; + var realmResourceId = $"realm:{realm.Id}"; + if (request.PictureId is not null) { var picture = await db.Files.FindAsync(request.PictureId); if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); - await fs.MarkUsageAsync(picture, 1); - if (realm.Picture is not null) await fs.MarkUsageAsync(realm.Picture, -1); - realm.Picture = picture; + + // Remove old references for the realm picture + if (realm.Picture is not null) + { + var oldPictureRefs = await fileRefService.GetResourceReferencesAsync(realmResourceId, "realm.picture"); + foreach (var oldRef in oldPictureRefs) + { + await fileRefService.DeleteReferenceAsync(oldRef.Id); + } + } + + realm.Picture = picture.ToReferenceObject(); + + // Create a new reference + await fileRefService.CreateReferenceAsync( + picture.Id, + "realm.picture", + realmResourceId + ); } if (request.BackgroundId is not null) { var background = await db.Files.FindAsync(request.BackgroundId); if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); - await fs.MarkUsageAsync(background, 1); - if (realm.Background is not null) await fs.MarkUsageAsync(realm.Background, -1); - realm.Background = background; + + // Remove old references for the realm background + if (realm.Background is not null) + { + var oldBackgroundRefs = + await fileRefService.GetResourceReferencesAsync(realmResourceId, "realm.background"); + foreach (var oldRef in oldBackgroundRefs) + { + await fileRefService.DeleteReferenceAsync(oldRef.Id); + } + } + + realm.Background = background.ToReferenceObject(); + + // Create a new reference + await fileRefService.CreateReferenceAsync( + background.Id, + "realm.background", + realmResourceId + ); } db.Realms.Update(realm); @@ -517,10 +576,9 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs, Re new Dictionary { { "realm_id", realm.Id } }, Request ); - if (realm.Picture is not null) - await fs.MarkUsageAsync(realm.Picture, -1); - if (realm.Background is not null) - await fs.MarkUsageAsync(realm.Background, -1); + // Delete all file references for this realm + var realmResourceId = $"realm:{realm.Id}"; + await fileRefService.DeleteResourceReferencesAsync(realmResourceId); return NoContent(); } diff --git a/DysonNetwork.Sphere/Sticker/Sticker.cs b/DysonNetwork.Sphere/Sticker/Sticker.cs index bc04a2e..878a0a5 100644 --- a/DysonNetwork.Sphere/Sticker/Sticker.cs +++ b/DysonNetwork.Sphere/Sticker/Sticker.cs @@ -1,20 +1,23 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using DysonNetwork.Sphere.Storage; using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Sphere.Sticker; [Index(nameof(Slug))] // The slug index shouldn't be unique, the sticker slug can be repeated across packs. -public class Sticker : ModelBase +public class Sticker : ModelBase, IIdentifiedResource { public Guid Id { get; set; } = Guid.NewGuid(); [MaxLength(128)] public string Slug { get; set; } = null!; [MaxLength(32)] public string ImageId { get; set; } = null!; - public CloudFile Image { get; set; } = null!; + [Column(TypeName = "jsonb")] public CloudFileReferenceObject Image { get; set; } = null!; public Guid PackId { get; set; } public StickerPack Pack { get; set; } = null!; + + public string ResourceIdentifier => $"sticker/{Id}"; } [Index(nameof(Prefix), IsUnique = true)] diff --git a/DysonNetwork.Sphere/Sticker/StickerController.cs b/DysonNetwork.Sphere/Sticker/StickerController.cs index ce1732f..5085d3a 100644 --- a/DysonNetwork.Sphere/Sticker/StickerController.cs +++ b/DysonNetwork.Sphere/Sticker/StickerController.cs @@ -294,7 +294,7 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa { Slug = request.Slug, ImageId = image.Id, - Image = image, + Image = image.ToReferenceObject(), Pack = pack }; diff --git a/DysonNetwork.Sphere/Sticker/StickerService.cs b/DysonNetwork.Sphere/Sticker/StickerService.cs index 8883b02..354989e 100644 --- a/DysonNetwork.Sphere/Sticker/StickerService.cs +++ b/DysonNetwork.Sphere/Sticker/StickerService.cs @@ -3,8 +3,10 @@ using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Sphere.Sticker; -public class StickerService(AppDatabase db, FileService fs, ICacheService cache) +public class StickerService(AppDatabase db, FileService fs, FileReferenceService fileRefService, ICacheService cache) { + public const string StickerFileUsageIdentifier = "sticker"; + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15); public async Task CreateStickerAsync(Sticker sticker) @@ -12,18 +14,37 @@ public class StickerService(AppDatabase db, FileService fs, ICacheService cache) db.Stickers.Add(sticker); await db.SaveChangesAsync(); - await fs.MarkUsageAsync(sticker.Image, 1); + var stickerResourceId = $"sticker:{sticker.Id}"; + await fileRefService.CreateReferenceAsync( + sticker.Image.Id, + StickerFileUsageIdentifier, + stickerResourceId + ); return sticker; } public async Task UpdateStickerAsync(Sticker sticker, CloudFile? newImage) { - if (newImage != null) + if (newImage is not null) { - await fs.MarkUsageAsync(sticker.Image, -1); - sticker.Image = newImage; - await fs.MarkUsageAsync(sticker.Image, 1); + var stickerResourceId = $"sticker:{sticker.Id}"; + + // Delete old references + var oldRefs = await fileRefService.GetResourceReferencesAsync(stickerResourceId, StickerFileUsageIdentifier); + foreach (var oldRef in oldRefs) + { + await fileRefService.DeleteReferenceAsync(oldRef.Id); + } + + sticker.Image = newImage.ToReferenceObject(); + + // Create new reference + await fileRefService.CreateReferenceAsync( + newImage.Id, + StickerFileUsageIdentifier, + stickerResourceId + ); } db.Stickers.Update(sticker); @@ -37,9 +58,13 @@ public class StickerService(AppDatabase db, FileService fs, ICacheService cache) public async Task DeleteStickerAsync(Sticker sticker) { + var stickerResourceId = $"sticker:{sticker.Id}"; + + // Delete all file references for this sticker + await fileRefService.DeleteResourceReferencesAsync(stickerResourceId); + db.Stickers.Remove(sticker); await db.SaveChangesAsync(); - await fs.MarkUsageAsync(sticker.Image, -1); // Invalidate cache for this sticker await PurgeStickerCache(sticker); @@ -54,12 +79,21 @@ public class StickerService(AppDatabase db, FileService fs, ICacheService cache) var images = stickers.Select(s => s.Image).ToList(); + // Delete all file references for each sticker in the pack + foreach (var sticker in stickers) + { + var stickerResourceId = $"sticker:{sticker.Id}"; + await fileRefService.DeleteResourceReferencesAsync(stickerResourceId); + } + + // Delete any references for the pack itself + var packResourceId = $"stickerpack:{pack.Id}"; + await fileRefService.DeleteResourceReferencesAsync(packResourceId); + db.Stickers.RemoveRange(stickers); db.StickerPacks.Remove(pack); await db.SaveChangesAsync(); - await fs.MarkUsageRangeAsync(images, -1); - // Invalidate cache for all stickers in this pack foreach (var sticker in stickers) await PurgeStickerCache(sticker); diff --git a/DysonNetwork.Sphere/Storage/CacheService.cs b/DysonNetwork.Sphere/Storage/CacheService.cs index acae560..ea05ab5 100644 --- a/DysonNetwork.Sphere/Storage/CacheService.cs +++ b/DysonNetwork.Sphere/Storage/CacheService.cs @@ -215,6 +215,7 @@ public class CacheServiceRedis : ICacheService public async Task SetAsync(string key, T value, TimeSpan? expiry = null) { + key = $"{GlobalKeyPrefix}{key}"; if (string.IsNullOrEmpty(key)) throw new ArgumentException("Key cannot be null or empty", nameof(key)); @@ -224,6 +225,7 @@ public class CacheServiceRedis : ICacheService public async Task GetAsync(string key) { + key = $"{GlobalKeyPrefix}{key}"; if (string.IsNullOrEmpty(key)) throw new ArgumentException("Key cannot be null or empty", nameof(key)); @@ -238,6 +240,7 @@ public class CacheServiceRedis : ICacheService public async Task<(bool found, T? value)> GetAsyncWithStatus(string key) { + key = $"{GlobalKeyPrefix}{key}"; if (string.IsNullOrEmpty(key)) throw new ArgumentException("Key cannot be null or empty", nameof(key)); @@ -252,6 +255,7 @@ public class CacheServiceRedis : ICacheService public async Task RemoveAsync(string key) { + key = $"{GlobalKeyPrefix}{key}"; if (string.IsNullOrEmpty(key)) throw new ArgumentException("Key cannot be null or empty", nameof(key)); @@ -281,6 +285,7 @@ public class CacheServiceRedis : ICacheService throw new ArgumentException(@"Group cannot be null or empty.", nameof(group)); var groupKey = $"{GroupKeyPrefix}{group}"; + key = $"{GlobalKeyPrefix}{key}"; await _database.SetAddAsync(groupKey, key); } @@ -319,6 +324,7 @@ public class CacheServiceRedis : ICacheService public async Task SetWithGroupsAsync(string key, T value, IEnumerable? groups = null, TimeSpan? expiry = null) { + key = $"{GlobalKeyPrefix}{key}"; // First, set the value in the cache var setResult = await SetAsync(key, value, expiry); diff --git a/DysonNetwork.Sphere/Storage/CloudFile.cs b/DysonNetwork.Sphere/Storage/CloudFile.cs index 5dbc5a7..8cd1e04 100644 --- a/DysonNetwork.Sphere/Storage/CloudFile.cs +++ b/DysonNetwork.Sphere/Storage/CloudFile.cs @@ -20,10 +20,28 @@ public class RemoteStorageConfig public string? AccessProxy { get; set; } } -public class CloudFile : ModelBase +/// +/// The class that used in jsonb columns which referenced the cloud file. +/// The aim of this class is to store some properties that won't change to a file to reduce the database load. +/// +public class CloudFileReferenceObject : ICloudFile +{ + public string Id { get; set; } = null!; + public string Name { get; set; } = string.Empty; + public Dictionary? FileMeta { get; set; } = null!; + public Dictionary? UserMeta { get; set; } = null!; + public string? MimeType { get; set; } + public string? Hash { get; set; } + public long Size { get; set; } + public bool HasCompression { get; set; } = false; +} + +public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource { /// The id generated by TuS, basically just UUID remove the dash lines - [MaxLength(32)] public string Id { get; set; } = Guid.NewGuid().ToString(); + [MaxLength(32)] + public string Id { get; set; } = Guid.NewGuid().ToString(); + [MaxLength(1024)] public string Name { get; set; } = string.Empty; [MaxLength(4096)] public string? Description { get; set; } [Column(TypeName = "jsonb")] public Dictionary? FileMeta { get; set; } = null!; @@ -33,29 +51,42 @@ public class CloudFile : ModelBase [MaxLength(256)] public string? Hash { get; set; } public long Size { get; set; } public Instant? UploadedAt { get; set; } - public Instant? ExpiredAt { get; set; } [MaxLength(128)] public string? UploadedTo { get; set; } - public bool HasCompression { get; set; }= false; - + public bool HasCompression { get; set; } = false; + /// The object name which stored remotely, /// multiple cloud file may have same storage id to indicate they are the same file /// /// If the storage id was null and the uploaded at is not null, means it is an embedding file, /// The embedding file means the file is store on another site, /// or it is a webpage (based on mimetype) - [MaxLength(32)] public string? StorageId { get; set; } + [MaxLength(32)] + public string? StorageId { get; set; } + /// This field should be null when the storage id is filled /// Indicates the off-site accessible url of the file - [MaxLength(4096)] public string? StorageUrl { get; set; } - - /// Metrics - /// When this used count keep zero, it means it's not used by anybody, so it can be recycled - public int UsedCount { get; set; } = 0; - /// An optional package identifier that indicates the cloud file's usage - [MaxLength(1024)] public string? Usage { get; set; } + [MaxLength(4096)] + public string? StorageUrl { get; set; } [JsonIgnore] public Account.Account Account { get; set; } = null!; public Guid AccountId { get; set; } + + public CloudFileReferenceObject ToReferenceObject() + { + return new CloudFileReferenceObject + { + Id = Id, + Name = Name, + FileMeta = FileMeta, + UserMeta = UserMeta, + MimeType = MimeType, + Hash = Hash, + Size = Size, + HasCompression = HasCompression + }; + } + + public string ResourceIdentifier => $"file/{Id}"; } public enum CloudFileSensitiveMark @@ -73,4 +104,18 @@ public enum CloudFileSensitiveMark SelfHarm, ChildAbuse, Other +} + +public class CloudFileReference : ModelBase +{ + public Guid Id { get; set; } = Guid.NewGuid(); + [MaxLength(32)] public string FileId { get; set; } = null!; + public CloudFile File { get; set; } = null!; + [MaxLength(1024)] public string Usage { get; set; } = null!; + [MaxLength(1024)] public string ResourceId { get; set; } = null!; + + /// + /// Optional expiration date for the file reference + /// + public Instant? ExpiredAt { get; set; } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Storage/CloudFileUnusedRecyclingJob.cs b/DysonNetwork.Sphere/Storage/CloudFileUnusedRecyclingJob.cs new file mode 100644 index 0000000..0a85a4a --- /dev/null +++ b/DysonNetwork.Sphere/Storage/CloudFileUnusedRecyclingJob.cs @@ -0,0 +1,127 @@ +using Microsoft.EntityFrameworkCore; +using NodaTime; +using Quartz; + +namespace DysonNetwork.Sphere.Storage; + +public class CloudFileUnusedRecyclingJob( + AppDatabase db, + FileService fs, + FileReferenceService fileRefService, + ILogger logger +) + : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + logger.LogInformation("Deleting unused cloud files..."); + + var cutoff = SystemClock.Instance.GetCurrentInstant() - Duration.FromHours(1); + var now = SystemClock.Instance.GetCurrentInstant(); + + // Get files that are either expired or created more than an hour ago + var fileIds = await db.Files + .Select(f => f.Id) + .ToListAsync(); + + // Filter to only include files that have no references or all references have expired + var deletionPlan = new List(); + foreach (var batch in fileIds.Chunk(100)) // Process in batches to avoid excessive query size + { + var references = await fileRefService.GetReferencesAsync(batch); + deletionPlan.AddRange(from refer in references + where refer.Value.Count == 0 || refer.Value.All(r => r.ExpiredAt != null && now >= r.ExpiredAt) + select refer.Key); + } + + if (deletionPlan.Count == 0) + { + logger.LogInformation("No files to delete"); + return; + } + + // Get the actual file objects for the files to be deleted + var files = await db.Files + .Where(f => deletionPlan.Contains(f.Id)) + .ToListAsync(); + + logger.LogInformation($"Found {files.Count} files to delete..."); + + // Group files by StorageId and find which ones are safe to delete + var storageIds = files.Where(f => f.StorageId != null) + .Select(f => f.StorageId!) + .Distinct() + .ToList(); + + // Check if any other files with the same storage IDs are referenced + var usedStorageIds = new List(); + var filesWithSameStorageId = await db.Files + .Where(f => f.StorageId != null && + storageIds.Contains(f.StorageId) && + !files.Select(ff => ff.Id).Contains(f.Id)) + .ToListAsync(); + + foreach (var file in filesWithSameStorageId) + { + // Get all references for the file + var references = await fileRefService.GetReferencesAsync(file.Id); + + // Check if file has active references (non-expired) + if (references.Any(r => r.ExpiredAt == null || r.ExpiredAt > now) && file.StorageId != null) + { + usedStorageIds.Add(file.StorageId); + } + } + + // Group files for deletion + var filesToDelete = files.Where(f => f.StorageId == null || !usedStorageIds.Contains(f.StorageId)) + .GroupBy(f => f.UploadedTo) + .ToDictionary(grouping => grouping.Key!, grouping => grouping.ToList()); + + // Delete files by remote storage + foreach (var group in filesToDelete.Where(group => !string.IsNullOrEmpty(group.Key))) + { + try + { + var dest = fs.GetRemoteStorageConfig(group.Key); + var client = fs.CreateMinioClient(dest); + if (client == null) continue; + + // Create delete tasks for each file in the group + var deleteTasks = group.Value.Select(file => + { + var objectId = file.StorageId ?? file.Id; + var tasks = new List + { + client.RemoveObjectAsync(new Minio.DataModel.Args.RemoveObjectArgs() + .WithBucket(dest.Bucket) + .WithObject(objectId)) + }; + + if (file.HasCompression) + { + tasks.Add(client.RemoveObjectAsync(new Minio.DataModel.Args.RemoveObjectArgs() + .WithBucket(dest.Bucket) + .WithObject(objectId + ".compressed"))); + } + + return Task.WhenAll(tasks); + }); + + await Task.WhenAll(deleteTasks); + } + catch (Exception ex) + { + logger.LogError(ex, "Error deleting files from remote storage {remote}", group.Key); + } + } + + // Delete all file records from the database + var fileIdsToDelete = files.Select(f => f.Id).ToList(); + await db.Files + .Where(f => fileIdsToDelete.Contains(f.Id)) + .ExecuteDeleteAsync(); + + logger.LogInformation($"Completed deleting {files.Count} files"); + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Storage/FileExpirationJob.cs b/DysonNetwork.Sphere/Storage/FileExpirationJob.cs new file mode 100644 index 0000000..50ccc17 --- /dev/null +++ b/DysonNetwork.Sphere/Storage/FileExpirationJob.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore; +using NodaTime; +using Quartz; + +namespace DysonNetwork.Sphere.Storage; + +/// +/// Job responsible for cleaning up expired file references +/// +public class FileExpirationJob(AppDatabase db, FileService fileService, ILogger logger) : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + var now = SystemClock.Instance.GetCurrentInstant(); + logger.LogInformation("Running file reference expiration job at {now}", now); + + // Find all expired references + var expiredReferences = await db.FileReferences + .Where(r => r.ExpiredAt < now && r.ExpiredAt != null) + .ToListAsync(); + + if (!expiredReferences.Any()) + { + logger.LogInformation("No expired file references found"); + return; + } + + logger.LogInformation("Found {count} expired file references", expiredReferences.Count); + + // Get unique file IDs + var fileIds = expiredReferences.Select(r => r.FileId).Distinct().ToList(); + var filesAndReferenceCount = new Dictionary(); + + // Delete expired references + db.FileReferences.RemoveRange(expiredReferences); + await db.SaveChangesAsync(); + + // Check remaining references for each file + foreach (var fileId in fileIds) + { + var remainingReferences = await db.FileReferences + .Where(r => r.FileId == fileId) + .CountAsync(); + + filesAndReferenceCount[fileId] = remainingReferences; + + // If no references remain, delete the file + if (remainingReferences == 0) + { + var file = await db.Files.FirstOrDefaultAsync(f => f.Id == fileId); + if (file != null) + { + logger.LogInformation("Deleting file {fileId} as all references have expired", fileId); + await fileService.DeleteFileAsync(file); + } + } + else + { + // Just purge the cache + await fileService._PurgeCacheAsync(fileId); + } + } + + logger.LogInformation("Completed file reference expiration job"); + } +} diff --git a/DysonNetwork.Sphere/Storage/FileReferenceService.cs b/DysonNetwork.Sphere/Storage/FileReferenceService.cs new file mode 100644 index 0000000..d2c4a5d --- /dev/null +++ b/DysonNetwork.Sphere/Storage/FileReferenceService.cs @@ -0,0 +1,407 @@ +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace DysonNetwork.Sphere.Storage; + +public class FileReferenceService(AppDatabase db, FileService fileService, ICacheService cache) +{ + private const string CacheKeyPrefix = "fileref:"; + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15); + + /// + /// Creates a new reference to a file for a specific resource + /// + /// The ID of the file to reference + /// The usage context (e.g., "avatar", "post-attachment") + /// The ID of the resource using the file + /// Optional expiration time for the file + /// Optional duration after which the file expires (alternative to expiredAt) + /// The created file reference + public async Task CreateReferenceAsync( + string fileId, + string usage, + string resourceId, + Instant? expiredAt = null, + Duration? duration = null) + { + // Calculate expiration time if needed + Instant? finalExpiration = expiredAt; + if (duration.HasValue) + { + finalExpiration = SystemClock.Instance.GetCurrentInstant() + duration.Value; + } + + var reference = new CloudFileReference + { + FileId = fileId, + Usage = usage, + ResourceId = resourceId, + ExpiredAt = finalExpiration + }; + + db.FileReferences.Add(reference); + + await db.SaveChangesAsync(); + + // Purge cache for the file since its usage count has effectively changed + await fileService._PurgeCacheAsync(fileId); + + return reference; + } + + /// + /// Gets all references to a file + /// + /// The ID of the file + /// A list of all references to the file + public async Task> GetReferencesAsync(string fileId) + { + var cacheKey = $"{CacheKeyPrefix}list:{fileId}"; + + var cachedReferences = await cache.GetAsync>(cacheKey); + if (cachedReferences is not null) + return cachedReferences; + + var references = await db.FileReferences + .Where(r => r.FileId == fileId) + .ToListAsync(); + + await cache.SetAsync(cacheKey, references, CacheDuration); + + return references; + } + + public async Task>> GetReferencesAsync(IEnumerable fileId) + { + var references = await db.FileReferences + .Where(r => fileId.Contains(r.FileId)) + .GroupBy(r => r.FileId) + .ToDictionaryAsync(r => r.Key, r => r.ToList()); + return references; + } + + /// + /// Gets the number of references to a file + /// + /// The ID of the file + /// The number of references to the file + public async Task GetReferenceCountAsync(string fileId) + { + var cacheKey = $"{CacheKeyPrefix}count:{fileId}"; + + var cachedCount = await cache.GetAsync(cacheKey); + if (cachedCount.HasValue) + return cachedCount.Value; + + var count = await db.FileReferences + .Where(r => r.FileId == fileId) + .CountAsync(); + + await cache.SetAsync(cacheKey, count, CacheDuration); + + return count; + } + + /// + /// Gets all references for a specific resource + /// + /// The ID of the resource + /// A list of file references associated with the resource + public async Task> GetResourceReferencesAsync(string resourceId) + { + var cacheKey = $"{CacheKeyPrefix}resource:{resourceId}"; + + var cachedReferences = await cache.GetAsync>(cacheKey); + if (cachedReferences is not null) + return cachedReferences; + + var references = await db.FileReferences + .Where(r => r.ResourceId == resourceId) + .ToListAsync(); + + await cache.SetAsync(cacheKey, references, CacheDuration); + + return references; + } + + /// + /// Gets all file references for a specific usage context + /// + /// The usage context + /// A list of file references with the specified usage + public async Task> GetUsageReferencesAsync(string usage) + { + return await db.FileReferences + .Where(r => r.Usage == usage) + .ToListAsync(); + } + + /// + /// Deletes references for a specific resource + /// + /// The ID of the resource + /// The number of deleted references + public async Task DeleteResourceReferencesAsync(string resourceId) + { + var references = await db.FileReferences + .Where(r => r.ResourceId == resourceId) + .ToListAsync(); + + var fileIds = references.Select(r => r.FileId).Distinct().ToList(); + + db.FileReferences.RemoveRange(references); + var deletedCount = await db.SaveChangesAsync(); + + // Purge caches + var tasks = fileIds.Select(fileService._PurgeCacheAsync).ToList(); + tasks.Add(PurgeCacheForResourceAsync(resourceId)); + await Task.WhenAll(tasks); + + return deletedCount; + } + + /// + /// Deletes a specific file reference + /// + /// The ID of the reference to delete + /// True if the reference was deleted, false otherwise + public async Task DeleteReferenceAsync(Guid referenceId) + { + var reference = await db.FileReferences + .FirstOrDefaultAsync(r => r.Id == referenceId); + + if (reference == null) + return false; + + db.FileReferences.Remove(reference); + await db.SaveChangesAsync(); + + // Purge caches + await fileService._PurgeCacheAsync(reference.FileId); + await PurgeCacheForResourceAsync(reference.ResourceId); + await PurgeCacheForFileAsync(reference.FileId); + + return true; + } + + /// + /// Updates the files referenced by a resource + /// + /// The ID of the resource + /// The new list of file IDs + /// The usage context + /// Optional expiration time for newly added files + /// Optional duration after which newly added files expire + /// A list of the updated file references + public async Task> UpdateResourceFilesAsync( + string resourceId, + IEnumerable? newFileIds, + string usage, + Instant? expiredAt = null, + Duration? duration = null) + { + if (newFileIds == null) + return new List(); + + var existingReferences = await db.FileReferences + .Where(r => r.ResourceId == resourceId && r.Usage == usage) + .ToListAsync(); + + var existingFileIds = existingReferences.Select(r => r.FileId).ToHashSet(); + var newFileIdsList = newFileIds.ToList(); + var newFileIdsSet = newFileIdsList.ToHashSet(); + + // Files to remove + var toRemove = existingReferences + .Where(r => !newFileIdsSet.Contains(r.FileId)) + .ToList(); + + // Files to add + var toAdd = newFileIdsList + .Where(id => !existingFileIds.Contains(id)) + .Select(id => new CloudFileReference + { + FileId = id, + Usage = usage, + ResourceId = resourceId + }) + .ToList(); + + // Apply changes + if (toRemove.Any()) + db.FileReferences.RemoveRange(toRemove); + + if (toAdd.Any()) + db.FileReferences.AddRange(toAdd); + + await db.SaveChangesAsync(); + + // Update expiration for newly added references if specified + if ((expiredAt.HasValue || duration.HasValue) && toAdd.Any()) + { + var finalExpiration = expiredAt; + if (duration.HasValue) + { + finalExpiration = SystemClock.Instance.GetCurrentInstant() + duration.Value; + } + + // Update newly added references with the expiration time + var referenceIds = await db.FileReferences + .Where(r => toAdd.Select(a => a.FileId).Contains(r.FileId) && + r.ResourceId == resourceId && + r.Usage == usage) + .Select(r => r.Id) + .ToListAsync(); + + await db.FileReferences + .Where(r => referenceIds.Contains(r.Id)) + .ExecuteUpdateAsync(setter => setter.SetProperty( + r => r.ExpiredAt, + _ => finalExpiration + )); + } + + // Purge caches + var allFileIds = existingFileIds.Union(newFileIdsSet).ToList(); + var tasks = allFileIds.Select(fileService._PurgeCacheAsync).ToList(); + tasks.Add(PurgeCacheForResourceAsync(resourceId)); + await Task.WhenAll(tasks); + + // Return updated references + return await db.FileReferences + .Where(r => r.ResourceId == resourceId && r.Usage == usage) + .ToListAsync(); + } + + /// + /// Gets all files referenced by a resource + /// + /// The ID of the resource + /// Optional filter by usage context + /// A list of files referenced by the resource + public async Task> GetResourceFilesAsync(string resourceId, string? usage = null) + { + var query = db.FileReferences.Where(r => r.ResourceId == resourceId); + + if (usage != null) + query = query.Where(r => r.Usage == usage); + + var references = await query.ToListAsync(); + var fileIds = references.Select(r => r.FileId).ToList(); + + return await db.Files + .Where(f => fileIds.Contains(f.Id)) + .ToListAsync(); + } + + /// + /// Purges all caches related to a resource + /// + private async Task PurgeCacheForResourceAsync(string resourceId) + { + var cacheKey = $"{CacheKeyPrefix}resource:{resourceId}"; + await cache.RemoveAsync(cacheKey); + } + + /// + /// Purges all caches related to a file + /// + private async Task PurgeCacheForFileAsync(string fileId) + { + var cacheKeys = new[] + { + $"{CacheKeyPrefix}list:{fileId}", + $"{CacheKeyPrefix}count:{fileId}" + }; + + var tasks = cacheKeys.Select(cache.RemoveAsync); + await Task.WhenAll(tasks); + } + + /// + /// Updates the expiration time for a file reference + /// + /// The ID of the reference + /// The new expiration time, or null to remove expiration + /// True if the reference was found and updated, false otherwise + public async Task SetReferenceExpirationAsync(Guid referenceId, Instant? expiredAt) + { + var reference = await db.FileReferences + .FirstOrDefaultAsync(r => r.Id == referenceId); + + if (reference == null) + return false; + + reference.ExpiredAt = expiredAt; + await db.SaveChangesAsync(); + + await PurgeCacheForFileAsync(reference.FileId); + await PurgeCacheForResourceAsync(reference.ResourceId); + + return true; + } + + /// + /// Updates the expiration time for all references to a file + /// + /// The ID of the file + /// The new expiration time, or null to remove expiration + /// The number of references updated + public async Task SetFileReferencesExpirationAsync(string fileId, Instant? expiredAt) + { + var rowsAffected = await db.FileReferences + .Where(r => r.FileId == fileId) + .ExecuteUpdateAsync(setter => setter.SetProperty( + r => r.ExpiredAt, + _ => expiredAt + )); + + if (rowsAffected > 0) + { + await fileService._PurgeCacheAsync(fileId); + await PurgeCacheForFileAsync(fileId); + } + + return rowsAffected; + } + + /// + /// Get all file references for a specific resource and usage type + /// + /// The resource ID + /// The usage type + /// List of file references + public async Task> GetResourceReferencesAsync(string resourceId, string usageType) + { + return await db.FileReferences + .Where(r => r.ResourceId == resourceId && r.Usage == usageType) + .ToListAsync(); + } + + /// + /// Check if a file has any references + /// + /// The file ID to check + /// True if the file has references, false otherwise + public async Task HasFileReferencesAsync(string fileId) + { + return await db.FileReferences.AnyAsync(r => r.FileId == fileId); + } + + /// + /// Updates the expiration time for a file reference using a duration from now + /// + /// The ID of the reference + /// The duration after which the reference expires, or null to remove expiration + /// True if the reference was found and updated, false otherwise + public async Task SetReferenceExpirationDurationAsync(Guid referenceId, Duration? duration) + { + Instant? expiredAt = null; + if (duration.HasValue) + { + expiredAt = SystemClock.Instance.GetCurrentInstant() + duration.Value; + } + + return await SetReferenceExpirationAsync(referenceId, expiredAt); + } +} diff --git a/DysonNetwork.Sphere/Storage/FileService.cs b/DysonNetwork.Sphere/Storage/FileService.cs index 3f46184..d31e91e 100644 --- a/DysonNetwork.Sphere/Storage/FileService.cs +++ b/DysonNetwork.Sphere/Storage/FileService.cs @@ -39,7 +39,10 @@ public class FileService( if (cachedFile is not null) return cachedFile; - var file = await db.Files.FirstOrDefaultAsync(f => f.Id == fileId); + var file = await db.Files + .Include(f => f.Account) + .Where(f => f.Id == fileId) + .FirstOrDefaultAsync(); if (file != null) await cache.SetAsync(cacheKey, file, CacheDuration); @@ -208,8 +211,9 @@ public class FileService( if (result.Count > 0) { List> tasks = []; - tasks.AddRange(result.Select(result => - nfs.UploadFileToRemoteAsync(file, result.filePath, null, result.suffix, true))); + tasks.AddRange(result.Select(item => + nfs.UploadFileToRemoteAsync(file, item.filePath, null, item.suffix, true)) + ); await Task.WhenAll(tasks); file = await tasks.First(); @@ -326,10 +330,23 @@ public class FileService( if (file.StorageId is null) return; if (file.UploadedTo is null) return; - var repeatedStorageId = await db.Files - .Where(f => f.StorageId == file.StorageId && f.Id != file.Id && f.UsedCount > 0) - .AnyAsync(); - if (repeatedStorageId) return; + // Check if any other file with the same storage ID is referenced + var otherFilesWithSameStorageId = await db.Files + .Where(f => f.StorageId == file.StorageId && f.Id != file.Id) + .Select(f => f.Id) + .ToListAsync(); + + // Check if any of these files are referenced + var anyReferenced = false; + if (otherFilesWithSameStorageId.Any()) + { + anyReferenced = await db.FileReferences + .Where(r => otherFilesWithSameStorageId.Contains(r.FileId)) + .AnyAsync(); + } + + // If any other file with the same storage ID is referenced, don't delete the actual file data + if (anyReferenced) return; var dest = GetRemoteStorageConfig(file.UploadedTo); var client = CreateMinioClient(dest); @@ -380,242 +397,88 @@ public class FileService( return client.Build(); } - - public async Task MarkUsageAsync(CloudFile file, int delta) - { - await db.Files.Where(o => o.Id == file.Id) - .ExecuteUpdateAsync(setter => setter.SetProperty( - b => b.UsedCount, - b => b.UsedCount + delta - ) - ); - - await _PurgeCacheAsync(file.Id); - } - - public async Task MarkUsageRangeAsync(ICollection files, int delta) - { - var ids = files.Select(f => f.Id).ToArray(); - await db.Files.Where(o => ids.Contains(o.Id)) - .ExecuteUpdateAsync(setter => setter.SetProperty( - b => b.UsedCount, - b => b.UsedCount + delta - ) - ); - - await _PurgeCacheRangeAsync(files.Select(x => x.Id).ToList()); - } - - - public async Task SetExpiresRangeAsync(ICollection files, Duration? duration) - { - var ids = files.Select(f => f.Id).ToArray(); - await db.Files.Where(o => ids.Contains(o.Id)) - .ExecuteUpdateAsync(setter => setter.SetProperty( - b => b.ExpiredAt, - duration.HasValue - ? b => SystemClock.Instance.GetCurrentInstant() + duration.Value - : _ => null - ) - ); - - await _PurgeCacheRangeAsync(files.Select(x => x.Id).ToList()); - } - - public async Task SetUsageAsync(CloudFile file, string? usage) - { - await db.Files.Where(o => o.Id == file.Id) - .ExecuteUpdateAsync(setter => setter.SetProperty( - b => b.Usage, - _ => usage - ) - ); - - await _PurgeCacheAsync(file.Id); - } - - public async Task SetUsageRangeAsync(ICollection files, string? usage) - { - var ids = files.Select(f => f.Id).ToArray(); - await db.Files.Where(o => ids.Contains(o.Id)) - .ExecuteUpdateAsync(setter => setter.SetProperty( - b => b.Usage, - _ => usage - ) - ); - - await _PurgeCacheRangeAsync(files.Select(x => x.Id).ToList()); - } - - public async Task<(ICollection current, ICollection added, ICollection removed)> - DiffAndSetUsageAsync( - ICollection? newFileIds, - string? usage, - ICollection? previousFiles = null - ) - { - if (newFileIds == null) return ([], [], previousFiles ?? []); - - var records = await db.Files.Where(f => newFileIds.Contains(f.Id)).ToListAsync(); - var previous = previousFiles?.ToDictionary(f => f.Id) ?? new Dictionary(); - var current = records.ToDictionary(f => f.Id); - - var added = current.Keys.Except(previous.Keys).Select(id => current[id]).ToList(); - var removed = previous.Keys.Except(current.Keys).Select(id => previous[id]).ToList(); - - if (added.Count > 0) await SetUsageRangeAsync(added, usage); - if (removed.Count > 0) await SetUsageRangeAsync(removed, null); - - return (newFileIds.Select(id => current[id]).ToList(), added, removed); - } - - public async Task<(ICollection current, ICollection added, ICollection removed)> - DiffAndMarkFilesAsync( - ICollection? newFileIds, - ICollection? previousFiles = null - ) - { - if (newFileIds == null) return ([], [], previousFiles ?? []); - - var records = await db.Files.Where(f => newFileIds.Contains(f.Id)).ToListAsync(); - var previous = previousFiles?.ToDictionary(f => f.Id) ?? new Dictionary(); - var current = records.ToDictionary(f => f.Id); - - var added = current.Keys.Except(previous.Keys).Select(id => current[id]).ToList(); - var removed = previous.Keys.Except(current.Keys).Select(id => previous[id]).ToList(); - - if (added.Count > 0) await MarkUsageRangeAsync(added, 1); - if (removed.Count > 0) await MarkUsageRangeAsync(removed, -1); - - return (newFileIds.Select(id => current[id]).ToList(), added, removed); - } - - public async Task<(ICollection current, ICollection added, ICollection removed)> - DiffAndSetExpiresAsync( - ICollection? newFileIds, - Duration? duration, - ICollection? previousFiles = null - ) - { - if (newFileIds == null) return ([], [], previousFiles ?? []); - - var records = await db.Files.Where(f => newFileIds.Contains(f.Id)).ToListAsync(); - var previous = previousFiles?.ToDictionary(f => f.Id) ?? new Dictionary(); - var current = records.ToDictionary(f => f.Id); - - var added = current.Keys.Except(previous.Keys).Select(id => current[id]).ToList(); - var removed = previous.Keys.Except(current.Keys).Select(id => previous[id]).ToList(); - - if (added.Count > 0) await SetExpiresRangeAsync(added, duration); - if (removed.Count > 0) await SetExpiresRangeAsync(removed, null); - - return (newFileIds.Select(id => current[id]).ToList(), added, removed); - } - - // Add this helper method to purge the cache for a specific file - private async Task _PurgeCacheAsync(string fileId) + + // Helper method to purge the cache for a specific file + // Made internal to allow FileReferenceService to use it + internal async Task _PurgeCacheAsync(string fileId) { var cacheKey = $"{CacheKeyPrefix}{fileId}"; await cache.RemoveAsync(cacheKey); } - // Add this helper method to purge cache for multiple files - private async Task _PurgeCacheRangeAsync(ICollection fileIds) + // Helper method to purge cache for multiple files + internal async Task _PurgeCacheRangeAsync(IEnumerable fileIds) { var tasks = fileIds.Select(_PurgeCacheAsync); await Task.WhenAll(tasks); } -} -public class CloudFileUnusedRecyclingJob(AppDatabase db, FileService fs, ILogger logger) - : IJob -{ - public async Task Execute(IJobExecutionContext context) + public async Task> LoadFromReference(List references) { - logger.LogInformation("Deleting unused cloud files..."); + var cachedFiles = new Dictionary(); + var uncachedIds = new List(); - var cutoff = SystemClock.Instance.GetCurrentInstant() - Duration.FromHours(1); - var now = SystemClock.Instance.GetCurrentInstant(); - - // Get files to delete along with their storage IDs - var files = await db.Files - .Where(f => - (f.ExpiredAt == null && f.UsedCount == 0 && f.CreatedAt < cutoff) || - (f.ExpiredAt != null && now >= f.ExpiredAt) - ) - .ToListAsync(); - - if (files.Count == 0) + // Check cache first + foreach (var reference in references) { - logger.LogInformation("No files to delete"); - return; + var cacheKey = $"{CacheKeyPrefix}{reference.Id}"; + var cachedFile = await cache.GetAsync(cacheKey); + + if (cachedFile != null) + { + cachedFiles[reference.Id] = cachedFile; + } + else + { + uncachedIds.Add(reference.Id); + } } - logger.LogInformation($"Found {files.Count} files to process..."); + // Load uncached files from database + if (uncachedIds.Count > 0) + { + var dbFiles = await db.Files + .Include(f => f.Account) + .Where(f => uncachedIds.Contains(f.Id)) + .ToListAsync(); - // Group files by StorageId and find which ones are safe to delete - var storageIds = files.Where(f => f.StorageId != null) - .Select(f => f.StorageId!) - .Distinct() + // Add to cache + foreach (var file in dbFiles) + { + var cacheKey = $"{CacheKeyPrefix}{file.Id}"; + await cache.SetAsync(cacheKey, file, CacheDuration); + cachedFiles[file.Id] = file; + } + } + + // Preserve original order + return references + .Select(r => cachedFiles.GetValueOrDefault(r.Id)) + .Where(f => f != null) .ToList(); - - var usedStorageIds = await db.Files - .Where(f => f.StorageId != null && - storageIds.Contains(f.StorageId) && - !files.Select(ff => ff.Id).Contains(f.Id)) - .Select(f => f.StorageId!) - .Distinct() - .ToListAsync(); - - // Group files for deletion - var filesToDelete = files.Where(f => f.StorageId == null || !usedStorageIds.Contains(f.StorageId)) - .GroupBy(f => f.UploadedTo) - .ToDictionary(grouping => grouping.Key!, grouping => grouping.ToList()); - - // Delete files by remote storage - foreach (var group in filesToDelete.Where(group => !string.IsNullOrEmpty(group.Key))) - { - try - { - var dest = fs.GetRemoteStorageConfig(group.Key); - var client = fs.CreateMinioClient(dest); - if (client == null) continue; - - // Create delete tasks for each file in the group - var deleteTasks = group.Value.Select(file => - { - var objectId = file.StorageId ?? file.Id; - var tasks = new List - { - client.RemoveObjectAsync(new RemoveObjectArgs() - .WithBucket(dest.Bucket) - .WithObject(objectId)) - }; - - if (file.HasCompression) - { - tasks.Add(client.RemoveObjectAsync(new RemoveObjectArgs() - .WithBucket(dest.Bucket) - .WithObject(objectId + ".compressed"))); - } - - return Task.WhenAll(tasks); - }); - - await Task.WhenAll(deleteTasks); - } - catch (Exception ex) - { - logger.LogError(ex, "Error deleting files from remote storage {remote}", group.Key); - } - } - - // Delete all file records from the database - var fileIds = files.Select(f => f.Id).ToList(); - await db.Files - .Where(f => fileIds.Contains(f.Id)) - .ExecuteDeleteAsync(); - - logger.LogInformation($"Completed deleting {files.Count} files"); } -} \ No newline at end of file + + /// + /// Gets the number of references to a file based on CloudFileReference records + /// + /// The ID of the file + /// The number of references to the file + public async Task GetReferenceCountAsync(string fileId) + { + return await db.FileReferences + .Where(r => r.FileId == fileId) + .CountAsync(); + } + + /// + /// Checks if a file is referenced by any resource + /// + /// The ID of the file to check + /// True if the file is referenced, false otherwise + public async Task IsReferencedAsync(string fileId) + { + return await db.FileReferences + .Where(r => r.FileId == fileId) + .AnyAsync(); + } +} diff --git a/DysonNetwork.Sphere/Storage/ICloudFile.cs b/DysonNetwork.Sphere/Storage/ICloudFile.cs new file mode 100644 index 0000000..fcdc9e8 --- /dev/null +++ b/DysonNetwork.Sphere/Storage/ICloudFile.cs @@ -0,0 +1,49 @@ +namespace DysonNetwork.Sphere.Storage; + +/// +/// Common interface for cloud file entities that can be used in file operations. +/// This interface exposes the essential properties needed for file operations +/// and is implemented by both CloudFile and CloudFileReferenceObject. +/// +public interface ICloudFile +{ + /// + /// Gets the unique identifier of the cloud file. + /// + string Id { get; } + + /// + /// Gets the name of the cloud file. + /// + string Name { get; } + + /// + /// Gets the file metadata dictionary. + /// + Dictionary? FileMeta { get; } + + /// + /// Gets the user metadata dictionary. + /// + Dictionary? UserMeta { get; } + + /// + /// Gets the MIME type of the file. + /// + string? MimeType { get; } + + /// + /// Gets the hash of the file content. + /// + string? Hash { get; } + + /// + /// Gets the size of the file in bytes. + /// + long Size { get; } + + /// + /// Gets whether the file has a compressed version available. + /// + bool HasCompression { get; } +} diff --git a/DysonNetwork.sln.DotSettings.user b/DysonNetwork.sln.DotSettings.user index 4500b73..74836b1 100644 --- a/DysonNetwork.sln.DotSettings.user +++ b/DysonNetwork.sln.DotSettings.user @@ -62,6 +62,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded