chore(codebase): full cleanup pass

This commit is contained in:
2025-06-21 01:58:48 -04:00
parent 8323477cd0
commit 81b5db34ef
92 changed files with 529 additions and 452 deletions

View File

@@ -3,6 +3,6 @@
public class CreatorOptions
{
public const string ConfigurationSection = "Creators";
public TimeSpan SlugReservationDuration { get; set; }
}

View File

@@ -5,16 +5,16 @@ namespace Hutopy.Modules.Creators.Data;
public class Creator
{
public Guid Id { get; set; }
public Guid CreatedBy { get; set; }
public DateTimeOffset CreatedAt { get; init; }
public Guid CreatedBy { get; set; }
public DateTimeOffset CreatedAt { get; init; }
public Guid? DeletedBy { get; set; }
public DateTimeOffset? DeletedAt { get; set; }
/// <summary>
/// Softdelete flag (false by default, true once DeletedAt is set)
/// Softdelete flag (false by default, true once DeletedAt is set)
/// </summary>
public bool IsDeleted { get; private set; } // private set → EF updates it
public bool IsDeleted { get; private set; } // private set → EF updates it
[MaxLength(2048)] public string? BannerUrl { get; set; }
[MaxLength(2048)] public string? PortraitUrl { get; set; }

View File

@@ -17,7 +17,7 @@ public class CreatorsDbContext(
modelBuilder
.Entity<Slugs>()
.Property(x => x.NormalizedName)
.HasComputedColumnSql("LOWER(\"Name\")", stored: true);
.HasComputedColumnSql("LOWER(\"Name\")", true);
modelBuilder
.Entity<Slugs>()
@@ -27,7 +27,7 @@ public class CreatorsDbContext(
modelBuilder
.Entity<Creator>()
.Property(c => c.IsDeleted)
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", stored: true); // bool
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true); // bool
modelBuilder
.Entity<Creator>()
@@ -38,7 +38,7 @@ public class CreatorsDbContext(
.Entity<Creator>()
.OwnsOne<Presentation>(x => x.Presentation)
.ToTable(nameof(Presentation));
modelBuilder
.Entity<Creator>()
.HasQueryFilter(c => !c.IsDeleted);

View File

@@ -12,4 +12,4 @@ public class Socials
[MaxLength(2048)] public string? YoutubeUrl { get; set; }
[MaxLength(2048)] public string? RedditUrl { get; set; }
[MaxLength(2048)] public string? WebsiteUrl { get; set; }
}
}

View File

@@ -14,21 +14,21 @@ public static class DependencyInjection
builder.Services.Configure<CreatorOptions>(
builder.Configuration.GetSection(CreatorOptions.ConfigurationSection));
builder.Services.AddScoped<SlugPurger>();
builder.Services.AddDbContext<CreatorsDbContext>(configureAction);
builder.Services.AddTransient<ICreatorLookup, CreatorLookup>();
return builder;
}
public static async Task<IApplicationBuilder> UseCreatorModuleAsync(
this IApplicationBuilder app,
CancellationToken cancellationToken = default)
{
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using var scope = scopeFactory.CreateScope();
await using var context = scope.ServiceProvider.GetRequiredService<CreatorsDbContext>();
await context.Database.MigrateAsync(cancellationToken: cancellationToken);
IServiceScopeFactory scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using IServiceScope scope = scopeFactory.CreateScope();
await using CreatorsDbContext context = scope.ServiceProvider.GetRequiredService<CreatorsDbContext>();
await context.Database.MigrateAsync(cancellationToken);
return app;
}

View File

@@ -29,11 +29,11 @@ public static class ChangeBanner
Request request,
CancellationToken ct)
{
var creator = await context
Creator? creator = await context
.Creators
.SingleOrDefaultAsync(
c => c.Id == request.CreatorId,
cancellationToken: ct);
ct);
if (creator is null)
{
@@ -41,7 +41,7 @@ public static class ChangeBanner
return;
}
var blobUrl = await blobStorage.UploadFileAsync(
string blobUrl = await blobStorage.UploadFileAsync(
ContainerNames.Creators,
$"{request.CreatorId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}",
request.File.OpenReadStream(),

View File

@@ -38,12 +38,12 @@ public class ChangeEmailHandler(
ChangeEmailRequest request,
CancellationToken ct)
{
var creator = await context
Creator? creator = await context
.Creators
.Include(c => c.Presentation)
.SingleOrDefaultAsync(
c => c.Id == request.CreatorId,
cancellationToken: ct);
ct);
if (creator is null)
{
@@ -59,9 +59,9 @@ public class ChangeEmailHandler(
}
creator.Presentation.Email = request.Email?.Trim();
await context.SaveChangesAsync(ct);
await SendOkAsync(ct);
}
}
}

View File

@@ -44,11 +44,11 @@ public class ChangeLogoHandler(
ChangeLogoRequest request,
CancellationToken ct)
{
var creator = await context
Creator? creator = await context
.Creators
.SingleOrDefaultAsync(
c => c.Id == request.CreatorId,
cancellationToken: ct);
ct);
if (creator is null)
{
@@ -56,7 +56,7 @@ public class ChangeLogoHandler(
return;
}
var blobUrl = await blobStorage.UploadFileAsync(
string blobUrl = await blobStorage.UploadFileAsync(
ContainerNames.Creators,
$"{request.CreatorId}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}",
request.File.OpenReadStream(),

View File

@@ -34,11 +34,11 @@ public class ChangeNameHandler(
ChangeNameRequest request,
CancellationToken ct)
{
var creator = await context
Creator creator = await context
.Creators
.SingleAsync(
c => c.Id == request.CreatorId,
cancellationToken: ct);
ct);
creator.Name = request.Name;

View File

@@ -38,12 +38,12 @@ public class ChangePhoneNumberHandler(
ChangePhoneNumberRequest request,
CancellationToken ct)
{
var creator = await context
Creator? creator = await context
.Creators
.Include(c => c.Presentation)
.SingleOrDefaultAsync(
c => c.Id == request.CreatorId,
cancellationToken: ct);
ct);
if (creator is null)
{
@@ -59,9 +59,9 @@ public class ChangePhoneNumberHandler(
}
creator.Presentation.PhoneNumber = request.PhoneNumber?.Trim();
await context.SaveChangesAsync(ct);
await SendOkAsync(ct);
}
}
}

View File

@@ -45,12 +45,12 @@ public class ChangePresentationInfosHandler(
ChangePresentationInfosRequest request,
CancellationToken ct)
{
var creator = await context
Creator? creator = await context
.Creators
.Include(c => c.Presentation)
.SingleOrDefaultAsync(
c => c.Id == request.CreatorId,
cancellationToken: ct);
ct);
if (creator is null)
{
@@ -60,12 +60,12 @@ public class ChangePresentationInfosHandler(
// Update the presentation info with the new values
creator.Presentation.Description = request.Description.Trim();
creator.Presentation.VideoUrl = request.VideoUrl != null
creator.Presentation.VideoUrl = request.VideoUrl != null
? YouTubeUrlHelper.ExtractVideoId(request.VideoUrl.Trim())
: null;
await context.SaveChangesAsync(ct);
await SendOkAsync(ct);
}
}

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Creators.Data;
using Microsoft.EntityFrameworkCore.Storage;
namespace Hutopy.Modules.Creators.Features;
@@ -17,7 +18,7 @@ internal sealed class ChangeSlugRequestValidator
RuleFor(r => r.CreatorId)
.NotNull().WithMessage("You should specify the CreatorId")
.NotEmpty().WithMessage("You should specify a valid/not empty CreatorId");
RuleFor(r => r.SlugReservationId)
.NotNull().WithMessage("You should specify the SlugReservationId")
.NotEmpty().WithMessage("You should specify a valid/not empty SlugReservationId");
@@ -39,15 +40,15 @@ public class ChangeSlugHandler(
ChangeSlugRequest request,
CancellationToken ct)
{
await using var transaction = await context.Database.BeginTransactionAsync(ct);
await using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync(ct);
try
{
var creator = await context
Creator creator = await context
.Creators
.SingleAsync(
c => c.Id == request.CreatorId,
cancellationToken: ct);
ct);
if (creator.CreatedBy != User.GetUserId())
{
@@ -55,7 +56,7 @@ public class ChangeSlugHandler(
return;
}
var reservation = await context
Slugs? reservation = await context
.Slugs
.FirstOrDefaultAsync(
s => s.Id == request.SlugReservationId,
@@ -67,7 +68,7 @@ public class ChangeSlugHandler(
return;
}
var previousReservation = await context
Slugs? previousReservation = await context
.Slugs
.FirstOrDefaultAsync(
s => s.UsedBy == request.CreatorId,

View File

@@ -27,12 +27,12 @@ public class ChangeSocialsHandler(
public override async Task HandleAsync(ChangeSocialsRequest request, CancellationToken ct)
{
var creator = await context
Creator creator = await context
.Creators
.Include(c => c.Socials)
.SingleAsync(
c => c.Id == request.CreatorId,
cancellationToken: ct);
ct);
creator.Socials.FacebookUrl = request.FacebookUrl;
creator.Socials.InstagramUrl = request.InstagramUrl;

View File

@@ -19,14 +19,14 @@ public class ChangeTitleHandler(
}
public override async Task HandleAsync(
ChangeTitleRequest request,
ChangeTitleRequest request,
CancellationToken ct)
{
var creator = await context
Creator creator = await context
.Creators
.SingleAsync(
c => c.Id == request.CreatorId,
cancellationToken: ct);
ct);
creator.Title = request.Title;

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Creators.Data;
using Microsoft.EntityFrameworkCore.Storage;
namespace Hutopy.Modules.Creators.Features;
@@ -17,7 +18,7 @@ public sealed class CreateCreatorRequestValidator : Validator<CreateCreatorReque
.NotNull()
.NotEmpty()
.WithMessage("You should specify a valid SlugReservationId");
RuleFor(r => r.CreatorId)
.NotNull()
.NotEmpty()
@@ -40,11 +41,11 @@ public sealed class CreateCreatorHandler(
CreateCreatorRequest req,
CancellationToken ct)
{
await using var transaction = await context.Database.BeginTransactionAsync(ct);
await using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync(ct);
try
{
var slug = await context
Slugs slug = await context
.Slugs
.SingleAsync(s => s.Id == req.SlugReservationId, ct);
@@ -55,23 +56,20 @@ public sealed class CreateCreatorHandler(
await SendErrorsAsync(500, ct);
return;
}
slug.UsedBy = req.CreatorId;
await context.Creators.AddAsync(
new Creator
{
Id = req.CreatorId,
CreatedBy = User.GetUserId(),
Name = slug.Name,
Slug = slug.NormalizedName
Id = req.CreatorId, CreatedBy = User.GetUserId(), Name = slug.Name, Slug = slug.NormalizedName
},
ct);
await context.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
await SendOkAsync(ct);
}
catch (Exception)

View File

@@ -28,7 +28,7 @@ public class GetCreatorByIdHandler(
public override void Configure()
{
Get("/api/creators/{CreatorId}");
Options((o => o.WithTags("Creators")));
Options(o => o.WithTags("Creators"));
AllowAnonymous();
}
@@ -36,13 +36,19 @@ public class GetCreatorByIdHandler(
GetCreatorByIdRequest req,
CancellationToken ct)
{
var creator = await context
Creator? creator = await context
.Creators
.FindAsync(
[req.CreatorId],
cancellationToken: ct);
ct);
if (creator is null) await SendNotFoundAsync(ct);
else await SendAsync(creator, cancellation: ct);
if (creator is null)
{
await SendNotFoundAsync(ct);
}
else
{
await SendAsync(creator, cancellation: ct);
}
}
}

View File

@@ -49,7 +49,7 @@ public class GetCreatorBySlugHandler(
public override void Configure()
{
Get("/api/creators/@{Name}");
Options((o => o.WithTags("Creators")));
Options(o => o.WithTags("Creators"));
AllowAnonymous();
}
@@ -57,9 +57,9 @@ public class GetCreatorBySlugHandler(
GetCreatorBySlugRequest req,
CancellationToken ct)
{
var creatorName = req.Name.ToLower();
string creatorName = req.Name.ToLower();
var response = await context
GetCreatorBySlugResponse? response = await context
.Creators
.IgnoreQueryFilters()
.Where(c => EF.Functions.ILike(c.Slug, creatorName))

View File

@@ -34,12 +34,12 @@ public sealed class RemoveCreatorHandler(
RemoveCreatorRequest req,
CancellationToken ct)
{
var creatorSlug = req.CreatorSlug.ToLower();
string creatorSlug = req.CreatorSlug.ToLower();
var creator = await context
Creator? creator = await context
.Creators
.Where(c => EF.Functions.ILike(c.Slug, creatorSlug))
.SingleOrDefaultAsync(cancellationToken: ct);
.SingleOrDefaultAsync(ct);
if (creator is null)
{

View File

@@ -4,6 +4,7 @@ using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Creators.Configuration;
using Hutopy.Modules.Creators.Data;
using Hutopy.Modules.Creators.Services;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Options;
using Npgsql;
@@ -45,33 +46,31 @@ public sealed class ReserveSlug(
ReserveSlugRequest req,
CancellationToken ct)
{
await using var transaction = await context.Database.BeginTransactionAsync(ct);
await using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync(ct);
try
{
// First, purge any expired slugs
await slugPurger.PurgeExpiredSlugsAsync(ct);
var reservation = await context.Slugs.FirstOrDefaultAsync(
s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(),
cancellationToken: ct);
Slugs? reservation = await context.Slugs.FirstOrDefaultAsync(
s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(),
ct);
if (reservation == null)
{
reservation = new Slugs
{
Id = req.ReservationId,
CreatedBy = User.GetUserId(),
CreatedAt = DateTimeOffset.UtcNow,
Id = req.ReservationId, CreatedBy = User.GetUserId(), CreatedAt = DateTimeOffset.UtcNow
};
context.Slugs.Attach(reservation);
context.Entry(reservation).State = EntityState.Added;
}
reservation.Name = req.Slug;
reservation.ReservedUntil = DateTimeOffset.UtcNow + opts.Value.SlugReservationDuration;
await context.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
@@ -81,7 +80,7 @@ public sealed class ReserveSlug(
catch (Exception e)
{
await transaction.RollbackAsync(ct);
Logger.LogError("Transaction failed: {Message}", e.Message);
if (e.InnerException is PostgresException innerException)

View File

@@ -34,13 +34,13 @@ public sealed class RestoreCreatorHandler(
RestoreCreatorRequest req,
CancellationToken ct)
{
var creatorSlug = req.CreatorSlug.ToLower();
string creatorSlug = req.CreatorSlug.ToLower();
var creator = await context
Creator? creator = await context
.Creators
.IgnoreQueryFilters()
.Where(c => EF.Functions.ILike(c.Slug, creatorSlug))
.SingleOrDefaultAsync(cancellationToken: ct);
.SingleOrDefaultAsync(ct);
if (creator is null)
{

View File

@@ -19,7 +19,7 @@ public class SlugPurger(CreatorsDbContext context)
try
{
var now = DateTimeOffset.UtcNow;
DateTimeOffset now = DateTimeOffset.UtcNow;
if (now - s_lastPurgeTime < MinTimeBetweenPurges)
{
// Not enough time has passed since the last purge
@@ -40,4 +40,4 @@ public class SlugPurger(CreatorsDbContext context)
Semaphore.Release();
}
}
}
}