+ Memberships
- DDD
- FutureCreator
- UserTransactions
This commit is contained in:
2024-10-20 14:01:58 -04:00
parent 3d10427821
commit 28d74503df
117 changed files with 2149 additions and 1999 deletions

View File

@@ -10,11 +10,11 @@ public class ContentDbContext(
public DbSet<Content> Contents => Set<Content>();
public DbSet<Creator> Creators => Set<Creator>();
public DbSet<Subscription> Subscriptions => Set<Subscription>();
public DbSet<Follower> Followers => Set<Follower>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("Content");
modelBuilder.HasDefaultSchema(SchemaName);
modelBuilder
.Entity<Content>()
@@ -34,13 +34,13 @@ public class ContentDbContext(
.ToTable(nameof(ContentReaction).Pluralize());
modelBuilder
.Entity<Subscription>()
.Entity<Follower>()
.HasOne(c => c.Creator)
.WithMany()
.HasForeignKey(c => c.CreatorId);
modelBuilder
.Entity<Subscription>()
.Entity<Follower>()
.HasKey(s => new { s.CreatedBy, s.CreatorId });
modelBuilder

View File

@@ -1,6 +1,6 @@
namespace Hutopy.Web.Features.Contents.Data;
public class Subscription
public class Follower
{
public Guid CreatedBy { get; init; }
public DateTimeOffset CreatedAt { get; init; }

View File

@@ -1,5 +1,5 @@
using Hutopy.Application.AzureBlobStorage.Constants;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Infrastructure.AzureBlob;
using Hutopy.Web.Features.Contents.Data;
namespace Hutopy.Web.Features.Contents.Handlers;
@@ -16,7 +16,7 @@ public record ChangeBannerResponse(
[PublicAPI]
public class ChangeBannerHandler(
ContentDbContext context,
IBlobStorage blobStorage)
AzureBlobStorage blobStorage)
: Endpoint<ChangeBannerRequest, ChangeBannerResponse>
{
public override void Configure()

View File

@@ -1,5 +1,5 @@
using Hutopy.Application.AzureBlobStorage.Constants;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Infrastructure.AzureBlob;
using Hutopy.Web.Features.Contents.Data;
namespace Hutopy.Web.Features.Contents.Handlers;
@@ -27,7 +27,7 @@ public sealed class ChangeLogoRequestValidator : Validator<ChangeLogoRequest>
[PublicAPI]
public class ChangeLogoHandler(
ContentDbContext context,
IBlobStorage blobStorage)
AzureBlobStorage blobStorage)
: Endpoint<ChangeLogoRequest>
{
public override void Configure()

View File

@@ -1,6 +1,6 @@
using System.Collections.Concurrent;
using Hutopy.Application.AzureBlobStorage.Constants;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Infrastructure.AzureBlob;
using Hutopy.Web.Common;
using Hutopy.Web.Features.Contents.Data;
using Hutopy.Web.Features.Contents.Handlers.Models;
@@ -44,7 +44,7 @@ public sealed class PostContentRequestValidator : Validator<PostContentRequest>
}
public sealed class PostContent(
IBlobStorage blobStorage,
AzureBlobStorage blobStorage,
ContentDbContext context)
: Endpoint<PostContentRequest>
{

View File

@@ -5,40 +5,40 @@ using Hutopy.Web.Features.Contents.Handlers.Models;
namespace Hutopy.Web.Features.Contents.Handlers;
[PublicAPI]
public sealed class SubscribeToCreatorRequest
public sealed class FollowCreatorRequest
{
public Guid CreatorId { get; set; }
}
[PublicAPI]
public sealed class SubscribeToCreatorHandler(
public sealed class FollowCreatorHandler(
ContentDbContext context)
: Endpoint<SubscribeToCreatorRequest, SubscriptionModel>
: Endpoint<FollowCreatorRequest, FollowModel>
{
public override void Configure()
{
Post("/api/creators/{CreatorId}/subscribe");
Options((o => o.WithTags("Subscriptions")));
Post("/api/creators/{CreatorId}/follow");
Options((o => o.WithTags("creators")));
Description(x => x.Accepts<string>("*/*"));
}
public override async Task HandleAsync(
SubscribeToCreatorRequest req,
FollowCreatorRequest req,
CancellationToken ct)
{
await context.Subscriptions.AddAsync(
new() { CreatedBy = HttpContext.User.GetUserId(), CreatorId = req.CreatorId },
await context.Followers.AddAsync(
new Follower { CreatedBy = User.GetUserId(), CreatorId = req.CreatorId },
ct);
await context.SaveChangesAsync(ct);
var creator = await context
.Creators
.Where(c => c.Id == req.CreatorId)
.Select(c => new SubscriptionModel(
.Where(creator => creator.Id == req.CreatorId)
.Select(creator => new FollowModel(
req.CreatorId,
c.Name,
c.Images.Logo
creator.Name,
creator.Images.Logo
))
.FirstOrDefaultAsync(cancellationToken: ct);

View File

@@ -50,7 +50,7 @@ public class GetCreatorByAliasHandler(
}
else
{
var subscriberCount = await context.Subscriptions.CountAsync(
var followerCount = await context.Followers.CountAsync(
s => s.CreatorId == creator.Id,
cancellationToken: ct);
@@ -63,7 +63,7 @@ public class GetCreatorByAliasHandler(
creator.Socials,
creator.Colors,
creator.Images,
subscriberCount);
followerCount);
await SendAsync(model, cancellation: ct);
}

View File

@@ -1,75 +0,0 @@
using Hutopy.Web.Common;
using Hutopy.Web.Extensions;
using Hutopy.Web.Features.Contents.Data;
using Hutopy.Web.Features.Contents.Handlers.Models;
namespace Hutopy.Web.Features.Contents.Handlers;
[PublicAPI]
public sealed class GetFollowedContentsRequest
{
[BindFrom("page_size")] public int PageSize { get; set; } = 10;
[BindFrom("last_id")] public Guid? LastId { get; set; }
}
[PublicAPI]
public class GetFollowedContentsHandler(
ContentDbContext context)
: Endpoint<GetFollowedContentsRequest, List<ContentModel>>
{
public override void Configure()
{
Get("/api/contents/followed");
Options(o => o.WithTags("Contents"));
AllowAnonymous();
}
public override async Task HandleAsync(
GetFollowedContentsRequest req,
CancellationToken ct)
{
var userId = HttpContext.User.GetUserId();
var userSubscriptionIds = await context
.Subscriptions
.Where(s => s.CreatedBy == userId)
.Select(s => s.CreatorId)
.ToListAsync(cancellationToken: ct);
var query = context.Contents
.Where(c => c.DeletedAt == null)
.Where(x => userSubscriptionIds.Contains(x.CreatedBy));
if (req.LastId.HasValue)
{
query = query.Where(c => c.Id > req.LastId.Value);
}
query = query.OrderByDescending(c => c.CreatedAt);
var content = await query
.Select(c => new ContentModel
{
Id = c.Id,
CreatedBy = c.CreatedBy,
CreatedByName = c.Creator!.Name,
CreatedByPortraitUrl = c.Creator.Images.Logo,
CreatedAt = c.CreatedAt,
DeletedBy = c.DeletedBy,
DeletedAt = c.DeletedAt,
Title = c.Title,
Description = c.Description,
Urls = c.Urls,
Reactions = c.Reactions.Select(x => new ReactionModel
{
Reaction = x.Reaction.FromEnum(),
UserId = x.UserId,
UserName = x.UserName
}).ToList()
})
.Take(req.PageSize)
.ToListAsync(ct);
await SendAsync(content, cancellation: ct);
}
}

View File

@@ -5,14 +5,14 @@ using Hutopy.Web.Features.Contents.Handlers.Models;
namespace Hutopy.Web.Features.Contents.Handlers;
[PublicAPI]
public class GetSubscriptionsHandler(
public class GetFollowedCreatorsHandler(
ContentDbContext context)
: EndpointWithoutRequest<List<SubscriptionModel>>
: EndpointWithoutRequest<List<FollowModel>>
{
public override void Configure()
{
Get("/api/subscriptions");
Options((o => o.WithTags("Subscriptions")));
Get("/api/creators/followed");
Options((o => o.WithTags("Creators")));
}
public override async Task HandleAsync(
@@ -21,9 +21,9 @@ public class GetSubscriptionsHandler(
var userId = HttpContext.User.GetUserId();
var subscriptions = await context
.Subscriptions
.Followers
.Where(s => s.CreatedBy == userId)
.Select(s => new SubscriptionModel(
.Select(s => new FollowModel(
s.CreatorId,
s.Creator!.Name,
s.Creator.Images.Logo))

View File

@@ -12,4 +12,4 @@ public record struct CreatorModel(
Socials Socials,
Colors Colors,
Images Images,
int SubscriberCount);
int FollowerCount);

View File

@@ -1,7 +1,7 @@
namespace Hutopy.Web.Features.Contents.Handlers.Models;
[PublicAPI]
public record SubscriptionModel(
public record FollowModel(
Guid CreatorId,
string CreatorName,
string? CreatorPortraitUrl);

View File

@@ -16,8 +16,8 @@ public class UnsubscribeFromCreatorHandler(
{
public override void Configure()
{
Post("/api/creators/{CreatorId}/unsubscribe");
Options((o => o.WithTags("Subscriptions")));
Post("/api/creators/{CreatorId}/unfollow");
Options((o => o.WithTags("Creators")));
Description(x => x.Accepts<string>("*/*"));
}
@@ -25,14 +25,15 @@ public class UnsubscribeFromCreatorHandler(
UnsubscribeFromCreatorRequest req,
CancellationToken ct)
{
var subscription = new Subscription { CreatorId = req.CreatorId, CreatedBy = HttpContext.User.GetUserId() };
context.Subscriptions.Attach(subscription);
context.Subscriptions.Remove(subscription);
try
{
var subscription = new Follower { CreatorId = req.CreatorId, CreatedBy = HttpContext.User.GetUserId() };
context.Followers.Attach(subscription);
context.Followers.Remove(subscription);
await context.SaveChangesAsync(ct);
await SendOkAsync(ct);
}
catch (Exception)

View File

@@ -0,0 +1,310 @@
// <auto-generated />
using System;
using Hutopy.Web.Features.Contents.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hutopy.Web.Features.Contents.Migrations
{
[DbContext(typeof(ContentDbContext))]
[Migration("20241011103653_FromSubscribersToFollowers")]
partial class FromSubscribersToFollowers
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Content")
.HasAnnotation("ProductVersion", "8.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uuid");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string[]>("Urls")
.HasColumnType("text[]");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.ToTable("Contents", "Content");
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Title")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.ToTable("Creators", "Content");
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Follower", b =>
{
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("CreatedBy", "CreatorId");
b.HasIndex("CreatorId");
b.ToTable("Followers", "Content");
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
{
b.HasOne("Hutopy.Web.Features.Contents.Data.Creator", "Creator")
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsMany("Hutopy.Web.Features.Contents.Data.ContentReaction", "Reactions", b1 =>
{
b1.Property<Guid>("ContentId")
.HasColumnType("uuid");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<int>("Reaction")
.HasColumnType("integer");
b1.Property<Guid>("UserId")
.HasColumnType("uuid");
b1.Property<string>("UserName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.HasKey("ContentId", "Id");
b1.ToTable("ContentReactions", "Content");
b1.WithOwner()
.HasForeignKey("ContentId");
});
b.Navigation("Creator");
b.Navigation("Reactions");
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
{
b.OwnsOne("Hutopy.Web.Features.Contents.Data.Colors", "Colors", b1 =>
{
b1.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b1.Property<string>("Background")
.IsRequired()
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("Error")
.IsRequired()
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("OnBackground")
.IsRequired()
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("OnError")
.IsRequired()
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("OnPrimary")
.IsRequired()
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("OnSecondary")
.IsRequired()
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("OnSurface")
.IsRequired()
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("Primary")
.IsRequired()
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("Secondary")
.IsRequired()
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("Surface")
.IsRequired()
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.HasKey("CreatorId");
b1.ToTable("Colors", "Content");
b1.WithOwner()
.HasForeignKey("CreatorId");
});
b.OwnsOne("Hutopy.Web.Features.Contents.Data.Images", "Images", b1 =>
{
b1.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b1.Property<string>("Banner")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("Logo")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.HasKey("CreatorId");
b1.ToTable("Images", "Content");
b1.WithOwner()
.HasForeignKey("CreatorId");
});
b.OwnsOne("Hutopy.Web.Features.Contents.Data.Socials", "Socials", b1 =>
{
b1.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b1.Property<string>("FacebookUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("InstagramUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("LinkedInUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("RedditUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("TikTokUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("WebsiteUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("XUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("YoutubeUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.HasKey("CreatorId");
b1.ToTable("Socials", "Content");
b1.WithOwner()
.HasForeignKey("CreatorId");
});
b.Navigation("Colors")
.IsRequired();
b.Navigation("Images")
.IsRequired();
b.Navigation("Socials")
.IsRequired();
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Follower", b =>
{
b.HasOne("Hutopy.Web.Features.Contents.Data.Creator", "Creator")
.WithMany()
.HasForeignKey("CreatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Creator");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Hutopy.Web.Features.Contents.Migrations
{
/// <inheritdoc />
public partial class FromSubscribersToFollowers : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Subscriptions",
schema: "Content");
migrationBuilder.CreateTable(
name: "Followers",
schema: "Content",
columns: table => new
{
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Followers", x => new { x.CreatedBy, x.CreatorId });
table.ForeignKey(
name: "FK_Followers_Creators_CreatorId",
column: x => x.CreatorId,
principalSchema: "Content",
principalTable: "Creators",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Followers_CreatorId",
schema: "Content",
table: "Followers",
column: "CreatorId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Followers",
schema: "Content");
migrationBuilder.CreateTable(
name: "Subscriptions",
schema: "Content",
columns: table => new
{
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Subscriptions", x => new { x.CreatedBy, x.CreatorId });
table.ForeignKey(
name: "FK_Subscriptions_Creators_CreatorId",
column: x => x.CreatorId,
principalSchema: "Content",
principalTable: "Creators",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Subscriptions_CreatorId",
schema: "Content",
table: "Subscriptions",
column: "CreatorId");
}
}
}

View File

@@ -92,7 +92,7 @@ namespace Hutopy.Web.Features.Contents.Migrations
b.ToTable("Creators", "Content");
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Subscription", b =>
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Follower", b =>
{
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
@@ -107,7 +107,7 @@ namespace Hutopy.Web.Features.Contents.Migrations
b.HasIndex("CreatorId");
b.ToTable("Subscriptions", "Content");
b.ToTable("Followers", "Content");
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
@@ -294,7 +294,7 @@ namespace Hutopy.Web.Features.Contents.Migrations
.IsRequired();
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Subscription", b =>
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Follower", b =>
{
b.HasOne("Hutopy.Web.Features.Contents.Data.Creator", "Creator")
.WithMany()