diff --git a/src/API/Controllers/Avatar/V1/AvatarController.cs b/src/API/Controllers/Avatar/V1/AvatarController.cs index eba798a..67d90d7 100644 --- a/src/API/Controllers/Avatar/V1/AvatarController.cs +++ b/src/API/Controllers/Avatar/V1/AvatarController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using RecNet.Application.Common.Security; using RecNet.Application.Profiles; +using RecNet.Application.Profiles.Avatar; using AvatarEntity = RecNet.Domain.Profiles.Avatar; namespace API.Controllers.Avatar.V1; diff --git a/src/API/Controllers/Profiles/V1/ProfilesController.cs b/src/API/Controllers/Profiles/V1/ProfilesController.cs index edfdbc0..96a3b29 100644 --- a/src/API/Controllers/Profiles/V1/ProfilesController.cs +++ b/src/API/Controllers/Profiles/V1/ProfilesController.cs @@ -1,6 +1,7 @@ using API.Contracts.Profiles.Responses; using Microsoft.AspNetCore.Mvc; using RecNet.Application.Profiles; +using RecNet.Application.Profiles.Login; using LoginRequest = API.Contracts.Profiles.Requests.LoginRequest; namespace API.Controllers.Profiles.V1; diff --git a/src/API/Controllers/Relationships/V1/RelationshipsController.cs b/src/API/Controllers/Relationships/V1/RelationshipsController.cs new file mode 100644 index 0000000..a4895c6 --- /dev/null +++ b/src/API/Controllers/Relationships/V1/RelationshipsController.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RecNet.Application.Common.Security; +using RecNet.Application.Profiles; +using RecNet.Application.Profiles.Relationships; + +namespace API.Controllers.Relationships.V1; + +[Authorize] +[ApiController] +[Route("api/[controller]/v1")] +public class RelationshipsController(IRelationshipService relationshipService) : ControllerBase +{ + [HttpGet("get")] + public async Task> GetRelationships(CancellationToken ct) + { + var profileId = User.GetProfileId(); + + return await relationshipService.GetRelationshipsAsync(profileId, ct); + } + + [HttpPost("update")] + public async Task> UpdateRelationship( + [FromBody] RelationshipDTO request, + CancellationToken ct) + { + var profileId = User.GetProfileId(); + + return Ok( + await relationshipService.UpdateRelationshipAsync + ( + profileId: profileId, + command: new UpdateRelationshipCommand(request.ProfileId, request.Muted, request.Ignored), + ct: ct + ) + ); + } +} \ No newline at end of file diff --git a/src/API/Controllers/Settings/V1/SettingsController.cs b/src/API/Controllers/Settings/V1/SettingsController.cs index 33bb235..e8d46ca 100644 --- a/src/API/Controllers/Settings/V1/SettingsController.cs +++ b/src/API/Controllers/Settings/V1/SettingsController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using RecNet.Application.Common.Security; using RecNet.Application.Profiles; +using RecNet.Application.Profiles.Settings; namespace API.Controllers.Settings.V1; diff --git a/src/RecNet.Application/Common/Mapping/ApplicationMappingProfile.cs b/src/RecNet.Application/Common/Mapping/ApplicationMappingProfile.cs index 62b811b..57d9866 100644 --- a/src/RecNet.Application/Common/Mapping/ApplicationMappingProfile.cs +++ b/src/RecNet.Application/Common/Mapping/ApplicationMappingProfile.cs @@ -1,5 +1,8 @@ using AutoMapper; using RecNet.Application.Profiles; +using RecNet.Application.Profiles.Avatar; +using RecNet.Application.Profiles.Relationships; +using RecNet.Application.Profiles.Settings; namespace RecNet.Application.Common.Mapping; @@ -9,6 +12,7 @@ public class ApplicationMappingProfile : Profile { CreateMap(); CreateMap(); + CreateMap(); CreateMap(); } } diff --git a/src/RecNet.Application/DependencyInjection.cs b/src/RecNet.Application/DependencyInjection.cs index 34fd06c..f720e4c 100644 --- a/src/RecNet.Application/DependencyInjection.cs +++ b/src/RecNet.Application/DependencyInjection.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using RecNet.Application.Neutrino; using RecNet.Application.Profiles; +using RecNet.Application.Profiles.Login; +using RecNet.Application.Profiles.Relationships; namespace RecNet.Application; @@ -13,6 +15,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); return services; diff --git a/src/RecNet.Application/Profiles/AvatarDto.cs b/src/RecNet.Application/Profiles/Avatar/AvatarDto.cs similarity index 88% rename from src/RecNet.Application/Profiles/AvatarDto.cs rename to src/RecNet.Application/Profiles/Avatar/AvatarDto.cs index bf721e0..2049b11 100644 --- a/src/RecNet.Application/Profiles/AvatarDto.cs +++ b/src/RecNet.Application/Profiles/Avatar/AvatarDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace RecNet.Application.Profiles; +namespace RecNet.Application.Profiles.Avatar; public class AvatarDTO { diff --git a/src/RecNet.Application/Profiles/UpdateAvatarCommand.cs b/src/RecNet.Application/Profiles/Avatar/UpdateAvatarCommand.cs similarity index 72% rename from src/RecNet.Application/Profiles/UpdateAvatarCommand.cs rename to src/RecNet.Application/Profiles/Avatar/UpdateAvatarCommand.cs index 58d8975..124e76b 100644 --- a/src/RecNet.Application/Profiles/UpdateAvatarCommand.cs +++ b/src/RecNet.Application/Profiles/Avatar/UpdateAvatarCommand.cs @@ -1,4 +1,4 @@ -namespace RecNet.Application.Profiles; +namespace RecNet.Application.Profiles.Avatar; public sealed record UpdateAvatarCommand( string? OutfitSelections, diff --git a/src/RecNet.Application/Profiles/IProfileService.cs b/src/RecNet.Application/Profiles/IProfileService.cs index 3ca9652..0f1dedb 100644 --- a/src/RecNet.Application/Profiles/IProfileService.cs +++ b/src/RecNet.Application/Profiles/IProfileService.cs @@ -1,4 +1,8 @@ +using RecNet.Application.Profiles.Avatar; +using RecNet.Application.Profiles.Login; +using RecNet.Application.Profiles.Settings; + namespace RecNet.Application.Profiles; public interface IProfileService diff --git a/src/RecNet.Application/Profiles/LoginProfileCommand.cs b/src/RecNet.Application/Profiles/Login/LoginProfileCommand.cs similarity index 82% rename from src/RecNet.Application/Profiles/LoginProfileCommand.cs rename to src/RecNet.Application/Profiles/Login/LoginProfileCommand.cs index faa29fe..861fd4c 100644 --- a/src/RecNet.Application/Profiles/LoginProfileCommand.cs +++ b/src/RecNet.Application/Profiles/Login/LoginProfileCommand.cs @@ -1,6 +1,6 @@ using RecNet.Domain.Common; -namespace RecNet.Application.Profiles; +namespace RecNet.Application.Profiles.Login; public sealed record LoginProfileCommand( string AppVersion, diff --git a/src/RecNet.Application/Profiles/LoginProfileResult.cs b/src/RecNet.Application/Profiles/Login/LoginProfileResult.cs similarity index 94% rename from src/RecNet.Application/Profiles/LoginProfileResult.cs rename to src/RecNet.Application/Profiles/Login/LoginProfileResult.cs index 0814c02..cb4a2d2 100644 --- a/src/RecNet.Application/Profiles/LoginProfileResult.cs +++ b/src/RecNet.Application/Profiles/Login/LoginProfileResult.cs @@ -1,4 +1,4 @@ -namespace RecNet.Application.Profiles; +namespace RecNet.Application.Profiles.Login; diff --git a/src/RecNet.Application/Profiles/NameGenerator.cs b/src/RecNet.Application/Profiles/Login/NameGenerator.cs similarity index 92% rename from src/RecNet.Application/Profiles/NameGenerator.cs rename to src/RecNet.Application/Profiles/Login/NameGenerator.cs index 17cd4dc..6b403e5 100644 --- a/src/RecNet.Application/Profiles/NameGenerator.cs +++ b/src/RecNet.Application/Profiles/Login/NameGenerator.cs @@ -1,7 +1,7 @@ using RecNet.Application.Common.Interfaces; using RecNet.Domain.Profiles.Names; -namespace RecNet.Application.Profiles; +namespace RecNet.Application.Profiles.Login; public class NameGenerator(IConfigService configService) { diff --git a/src/RecNet.Application/Profiles/ProfileService.cs b/src/RecNet.Application/Profiles/ProfileService.cs index 6a1bf16..87cd91f 100644 --- a/src/RecNet.Application/Profiles/ProfileService.cs +++ b/src/RecNet.Application/Profiles/ProfileService.cs @@ -1,10 +1,14 @@ using AutoMapper; using RecNet.Application.Common.Interfaces; using RecNet.Application.Common.Security; +using RecNet.Application.Profiles.Avatar; +using RecNet.Application.Profiles.Login; +using RecNet.Application.Profiles.Settings; using RecNet.Domain.Common; using RecNet.Domain.GameVersions; using RecNet.Domain.Profiles; using ProfileEntity = RecNet.Domain.Profiles.Profile; +using AvatarEntity = RecNet.Domain.Profiles.Avatar; namespace RecNet.Application.Profiles; @@ -45,7 +49,7 @@ public class ProfileService( return null; profile.SetAvatar( - Avatar.Create( + AvatarEntity.Create( command.OutfitSelections, command.SkinColor, command.HairColor diff --git a/src/RecNet.Application/Profiles/Relationships/IRelationshipService.cs b/src/RecNet.Application/Profiles/Relationships/IRelationshipService.cs new file mode 100644 index 0000000..4bfe7fb --- /dev/null +++ b/src/RecNet.Application/Profiles/Relationships/IRelationshipService.cs @@ -0,0 +1,7 @@ +namespace RecNet.Application.Profiles.Relationships; + +public interface IRelationshipService +{ + Task> GetRelationshipsAsync(Guid profileId, CancellationToken ct = default); + Task UpdateRelationshipAsync(Guid profileId, UpdateRelationshipCommand command, CancellationToken ct = default); +} \ No newline at end of file diff --git a/src/RecNet.Application/Profiles/Relationships/RelationshipDto.cs b/src/RecNet.Application/Profiles/Relationships/RelationshipDto.cs new file mode 100644 index 0000000..98355f1 --- /dev/null +++ b/src/RecNet.Application/Profiles/Relationships/RelationshipDto.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace RecNet.Application.Profiles.Relationships; + +public class RelationshipDTO +{ + [JsonPropertyName("ProfileId")] + public Guid ProfileId { get; set; } + + [JsonPropertyName("Muted")] + public bool Muted { get; set; } + + [JsonPropertyName("Ignored")] + public bool Ignored { get; set; } +} \ No newline at end of file diff --git a/src/RecNet.Application/Profiles/Relationships/RelationshipService.cs b/src/RecNet.Application/Profiles/Relationships/RelationshipService.cs new file mode 100644 index 0000000..822542c --- /dev/null +++ b/src/RecNet.Application/Profiles/Relationships/RelationshipService.cs @@ -0,0 +1,33 @@ +using AutoMapper; +using RecNet.Domain.Profiles; + +namespace RecNet.Application.Profiles.Relationships; + +public class RelationshipService(IRelationshipRepository repository, IMapper mapper) : IRelationshipService +{ + public async Task> GetRelationshipsAsync( + Guid profileId, + CancellationToken ct = default) + { + var relationships = await repository.GetAllOwnedRelationships(profileId, ct); + + return mapper.Map>(relationships); + } + + public async Task UpdateRelationshipAsync(Guid profileId, UpdateRelationshipCommand command, CancellationToken ct = default) + { + var relationship = await repository.GetAsync(profileId, command.ProfileId, ct); + if (relationship == null) + { + + relationship = Relationship.Create(profileId, command.ProfileId); + await repository.AddAsync(relationship, ct); + } + + relationship.Mutate(command.Muted, command.Ignored); + + await repository.SaveChangesAsync(ct); + + return mapper.Map(relationship); + } +} diff --git a/src/RecNet.Application/Profiles/Relationships/UpdateRelationshipCommand.cs b/src/RecNet.Application/Profiles/Relationships/UpdateRelationshipCommand.cs new file mode 100644 index 0000000..bb3cb83 --- /dev/null +++ b/src/RecNet.Application/Profiles/Relationships/UpdateRelationshipCommand.cs @@ -0,0 +1,6 @@ +namespace RecNet.Application.Profiles.Relationships; + +public record UpdateRelationshipCommand( + Guid ProfileId, + bool Muted, + bool Ignored); \ No newline at end of file diff --git a/src/RecNet.Application/Profiles/PlayerSettingDto.cs b/src/RecNet.Application/Profiles/Settings/PlayerSettingDto.cs similarity index 83% rename from src/RecNet.Application/Profiles/PlayerSettingDto.cs rename to src/RecNet.Application/Profiles/Settings/PlayerSettingDto.cs index d03c776..c86a044 100644 --- a/src/RecNet.Application/Profiles/PlayerSettingDto.cs +++ b/src/RecNet.Application/Profiles/Settings/PlayerSettingDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace RecNet.Application.Profiles; +namespace RecNet.Application.Profiles.Settings; public class PlayerSettingDTO { diff --git a/src/RecNet.Application/Profiles/UpdatePlayerSettingCommand.cs b/src/RecNet.Application/Profiles/Settings/UpdatePlayerSettingCommand.cs similarity index 62% rename from src/RecNet.Application/Profiles/UpdatePlayerSettingCommand.cs rename to src/RecNet.Application/Profiles/Settings/UpdatePlayerSettingCommand.cs index 97cf3fe..59ed175 100644 --- a/src/RecNet.Application/Profiles/UpdatePlayerSettingCommand.cs +++ b/src/RecNet.Application/Profiles/Settings/UpdatePlayerSettingCommand.cs @@ -1,4 +1,4 @@ -namespace RecNet.Application.Profiles; +namespace RecNet.Application.Profiles.Settings; public sealed record UpdatePlayerSettingCommand( string Key, diff --git a/src/RecNet.Domain/Profiles/IRelationshipRepository.cs b/src/RecNet.Domain/Profiles/IRelationshipRepository.cs new file mode 100644 index 0000000..a66469b --- /dev/null +++ b/src/RecNet.Domain/Profiles/IRelationshipRepository.cs @@ -0,0 +1,10 @@ +namespace RecNet.Domain.Profiles; + +public interface IRelationshipRepository +{ + Task GetAsync(Guid ownerId, Guid profileId, CancellationToken ct = default); + Task> GetAllOwnedRelationships(Guid ownerId, CancellationToken ct = default); + + Task AddAsync(Relationship relationship, CancellationToken ct = default); + Task SaveChangesAsync(CancellationToken ct = default); +} \ No newline at end of file diff --git a/src/RecNet.Domain/Profiles/Profile.cs b/src/RecNet.Domain/Profiles/Profile.cs index a1cba2b..10aa9be 100644 --- a/src/RecNet.Domain/Profiles/Profile.cs +++ b/src/RecNet.Domain/Profiles/Profile.cs @@ -39,6 +39,7 @@ public class Profile public Avatar Avatar { get; private set; } = Avatar.Empty; public ICollection Settings { get; } = []; + public ICollection Relationships { get; } = []; public static Profile Create( string name, diff --git a/src/RecNet.Domain/Profiles/Relationship.cs b/src/RecNet.Domain/Profiles/Relationship.cs new file mode 100644 index 0000000..1e3743a --- /dev/null +++ b/src/RecNet.Domain/Profiles/Relationship.cs @@ -0,0 +1,35 @@ +using RecNet.Domain.Exceptions; + +namespace RecNet.Domain.Profiles; + +public class Relationship +{ + public Guid OwnerProfileId { get; private set; } + + public Guid ProfileId { get; private set; } + + public bool Muted { get; private set; } + public bool Ignored { get; private set; } + + private Relationship() + { + } + + public static Relationship Create(Guid ownerProfileId, Guid profileId) + { + if (ownerProfileId == Guid.Empty || profileId == Guid.Empty) + throw new DomainException("Profile IDs cannot be null."); + + return new Relationship + { + OwnerProfileId = ownerProfileId, + ProfileId = profileId + }; + } + + public void Mutate(bool muted, bool ignored) + { + Muted = muted; + Ignored = ignored; + } +} \ No newline at end of file diff --git a/src/RecNet.Infrastructure/DependencyInjection.cs b/src/RecNet.Infrastructure/DependencyInjection.cs index 9ff008d..4c0a987 100644 --- a/src/RecNet.Infrastructure/DependencyInjection.cs +++ b/src/RecNet.Infrastructure/DependencyInjection.cs @@ -40,6 +40,7 @@ public static class DependencyInjection builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/RecNet.Infrastructure/Persistence/Configurations/ProfileConfiguration.cs b/src/RecNet.Infrastructure/Persistence/Configurations/ProfileConfiguration.cs index d31dd9f..6749748 100644 --- a/src/RecNet.Infrastructure/Persistence/Configurations/ProfileConfiguration.cs +++ b/src/RecNet.Infrastructure/Persistence/Configurations/ProfileConfiguration.cs @@ -35,6 +35,13 @@ public class ProfileConfiguration : IEntityTypeConfiguration builder.Navigation(x => x.Settings) .UsePropertyAccessMode(PropertyAccessMode.Field); + + builder.HasMany(x => x.Relationships) + .WithOne() + .HasForeignKey(x => x.OwnerProfileId); + + builder.Navigation(x => x.Relationships) + .UsePropertyAccessMode(PropertyAccessMode.Field); builder.OwnsOne(x => x.Avatar); diff --git a/src/RecNet.Infrastructure/Persistence/Configurations/RelationshipConfiguration.cs b/src/RecNet.Infrastructure/Persistence/Configurations/RelationshipConfiguration.cs new file mode 100644 index 0000000..974048f --- /dev/null +++ b/src/RecNet.Infrastructure/Persistence/Configurations/RelationshipConfiguration.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using RecNet.Domain.Profiles; + +namespace RecNet.Infrastructure.Persistence.Configurations; + +public class RelationshipConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => new { x.OwnerProfileId, x.ProfileId }); + + builder.Property(x => x.OwnerProfileId) + .IsRequired(); + + builder.Property(x => x.ProfileId) + .IsRequired(); + + builder.HasOne() + .WithMany() + .HasForeignKey(x => x.ProfileId) + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + } +} diff --git a/src/RecNet.Infrastructure/Persistence/DatabaseContext.cs b/src/RecNet.Infrastructure/Persistence/DatabaseContext.cs index 8b7d3ea..7346d90 100644 --- a/src/RecNet.Infrastructure/Persistence/DatabaseContext.cs +++ b/src/RecNet.Infrastructure/Persistence/DatabaseContext.cs @@ -9,6 +9,7 @@ public class DatabaseContext(DbContextOptions options) : DbCont { public DbSet Profiles => Set(); public DbSet PlayerSettings => Set(); + public DbSet Relationships => Set(); public DbSet GameVersions => Set(); public DbSet ServerConfigs => Set(); diff --git a/src/RecNet.Infrastructure/Persistence/Migrations/20260626215037_Relationships.Designer.cs b/src/RecNet.Infrastructure/Persistence/Migrations/20260626215037_Relationships.Designer.cs new file mode 100644 index 0000000..781f838 --- /dev/null +++ b/src/RecNet.Infrastructure/Persistence/Migrations/20260626215037_Relationships.Designer.cs @@ -0,0 +1,209 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RecNet.Infrastructure.Persistence; + +#nullable disable + +namespace RecNet.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260626215037_Relationships")] + partial class Relationships + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RecNet.Domain.Configuration.ServerConfig", b => + { + b.Property("Key") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Key"); + + b.ToTable("ServerConfigs"); + }); + + modelBuilder.Entity("RecNet.Domain.GameVersions.GameVersion", b => + { + b.Property("Version") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IsValid") + .HasColumnType("boolean"); + + b.HasKey("Version"); + + b.ToTable("GameVersions"); + }); + + modelBuilder.Entity("RecNet.Domain.Profiles.PlayerSetting", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UserId", "Key"); + + b.ToTable("PlayerSettings"); + }); + + modelBuilder.Entity("RecNet.Domain.Profiles.Profile", b => + { + b.Property("ProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.PrimitiveCollection("DeviceIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("IsBanned") + .HasColumnType("boolean"); + + b.Property("IsDeveloper") + .HasColumnType("boolean"); + + b.Property("IsModerator") + .HasColumnType("boolean"); + + b.Property("MetaAuthenticationSecret") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlatformId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("ProfileId"); + + b.HasIndex("Platform", "PlatformId") + .IsUnique(); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("RecNet.Domain.Profiles.Relationship", b => + { + b.Property("OwnerProfileId") + .HasColumnType("uuid"); + + b.Property("ProfileId") + .HasColumnType("uuid"); + + b.Property("Ignored") + .HasColumnType("boolean"); + + b.Property("Muted") + .HasColumnType("boolean"); + + b.HasKey("OwnerProfileId", "ProfileId"); + + b.HasIndex("ProfileId"); + + b.ToTable("Relationships"); + }); + + modelBuilder.Entity("RecNet.Domain.Profiles.PlayerSetting", b => + { + b.HasOne("RecNet.Domain.Profiles.Profile", null) + .WithMany("Settings") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RecNet.Domain.Profiles.Profile", b => + { + b.OwnsOne("RecNet.Domain.Profiles.Avatar", "Avatar", b1 => + { + b1.Property("ProfileId") + .HasColumnType("uuid"); + + b1.Property("HairColor") + .IsRequired() + .HasColumnType("text"); + + b1.Property("OutfitSelections") + .IsRequired() + .HasColumnType("text"); + + b1.Property("SkinColor") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("ProfileId"); + + b1.ToTable("Profiles"); + + b1.WithOwner() + .HasForeignKey("ProfileId"); + }); + + b.Navigation("Avatar") + .IsRequired(); + }); + + modelBuilder.Entity("RecNet.Domain.Profiles.Relationship", b => + { + b.HasOne("RecNet.Domain.Profiles.Profile", null) + .WithMany("Relationships") + .HasForeignKey("OwnerProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RecNet.Domain.Profiles.Profile", null) + .WithMany() + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RecNet.Domain.Profiles.Profile", b => + { + b.Navigation("Relationships"); + + b.Navigation("Settings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/RecNet.Infrastructure/Persistence/Migrations/20260626215037_Relationships.cs b/src/RecNet.Infrastructure/Persistence/Migrations/20260626215037_Relationships.cs new file mode 100644 index 0000000..46bb3d6 --- /dev/null +++ b/src/RecNet.Infrastructure/Persistence/Migrations/20260626215037_Relationships.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RecNet.Infrastructure.Persistence.Migrations +{ + /// + public partial class Relationships : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Relationships", + columns: table => new + { + OwnerProfileId = table.Column(type: "uuid", nullable: false), + ProfileId = table.Column(type: "uuid", nullable: false), + Muted = table.Column(type: "boolean", nullable: false), + Ignored = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Relationships", x => new { x.OwnerProfileId, x.ProfileId }); + table.ForeignKey( + name: "FK_Relationships_Profiles_OwnerProfileId", + column: x => x.OwnerProfileId, + principalTable: "Profiles", + principalColumn: "ProfileId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Relationships_Profiles_ProfileId", + column: x => x.ProfileId, + principalTable: "Profiles", + principalColumn: "ProfileId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Relationships_ProfileId", + table: "Relationships", + column: "ProfileId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Relationships"); + } + } +} diff --git a/src/RecNet.Infrastructure/Persistence/Migrations/DatabaseContextModelSnapshot.cs b/src/RecNet.Infrastructure/Persistence/Migrations/DatabaseContextModelSnapshot.cs index 04fa6b1..6c4e558 100644 --- a/src/RecNet.Infrastructure/Persistence/Migrations/DatabaseContextModelSnapshot.cs +++ b/src/RecNet.Infrastructure/Persistence/Migrations/DatabaseContextModelSnapshot.cs @@ -118,6 +118,27 @@ namespace RecNet.Infrastructure.Persistence.Migrations b.ToTable("Profiles"); }); + modelBuilder.Entity("RecNet.Domain.Profiles.Relationship", b => + { + b.Property("OwnerProfileId") + .HasColumnType("uuid"); + + b.Property("ProfileId") + .HasColumnType("uuid"); + + b.Property("Ignored") + .HasColumnType("boolean"); + + b.Property("Muted") + .HasColumnType("boolean"); + + b.HasKey("OwnerProfileId", "ProfileId"); + + b.HasIndex("ProfileId"); + + b.ToTable("Relationships"); + }); + modelBuilder.Entity("RecNet.Domain.Profiles.PlayerSetting", b => { b.HasOne("RecNet.Domain.Profiles.Profile", null) @@ -158,8 +179,25 @@ namespace RecNet.Infrastructure.Persistence.Migrations .IsRequired(); }); + modelBuilder.Entity("RecNet.Domain.Profiles.Relationship", b => + { + b.HasOne("RecNet.Domain.Profiles.Profile", null) + .WithMany("Relationships") + .HasForeignKey("OwnerProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RecNet.Domain.Profiles.Profile", null) + .WithMany() + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("RecNet.Domain.Profiles.Profile", b => { + b.Navigation("Relationships"); + b.Navigation("Settings"); }); #pragma warning restore 612, 618 diff --git a/src/RecNet.Infrastructure/Persistence/Repositories/RelationshipRepository.cs b/src/RecNet.Infrastructure/Persistence/Repositories/RelationshipRepository.cs new file mode 100644 index 0000000..0ce7b83 --- /dev/null +++ b/src/RecNet.Infrastructure/Persistence/Repositories/RelationshipRepository.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore; +using RecNet.Domain.Profiles; + +namespace RecNet.Infrastructure.Persistence.Repositories; + +public class RelationshipRepository(DatabaseContext dbContext) : IRelationshipRepository +{ + public Task GetAsync( + Guid ownerId, + Guid profileId, + CancellationToken ct = default) + => dbContext.Relationships + .FirstOrDefaultAsync(x => + x.OwnerProfileId == ownerId && + x.ProfileId == profileId, + ct); + + public async Task> GetAllOwnedRelationships(Guid ownerId, CancellationToken ct = default) + => await dbContext.Relationships + .AsNoTracking() + .Where(x => x.OwnerProfileId == ownerId) + .ToListAsync(ct); + + public async Task AddAsync( + Relationship relationship, + CancellationToken ct = default) + => await dbContext.Relationships.AddAsync(relationship, ct); + + public Task SaveChangesAsync(CancellationToken ct = default) + => dbContext.SaveChangesAsync(ct); +} \ No newline at end of file