refactor: contain backend feature mappings
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-04-25 01:14:01 -04:00
parent 121757546a
commit 20f8a14bfb
10 changed files with 308 additions and 190 deletions

View File

@@ -32,195 +32,13 @@ public class AppDbContext(
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Workspace>(workspace =>
{
workspace.ToTable("Workspaces");
workspace.HasKey(x => x.Id);
workspace.Property(x => x.Name).HasMaxLength(256).IsRequired();
workspace.Property(x => x.Slug).HasMaxLength(128).IsRequired();
workspace.Property(x => x.TimeZone).HasMaxLength(128).IsRequired();
workspace.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
workspace.HasIndex(x => x.Slug).IsUnique();
workspace.HasIndex(x => x.OwnerUserId);
});
modelBuilder.Entity<WorkspaceInvite>(workspaceInvite =>
{
workspaceInvite.ToTable("WorkspaceInvites");
workspaceInvite.HasKey(x => x.Id);
workspaceInvite.Property(x => x.Email).HasMaxLength(256).IsRequired();
workspaceInvite.Property(x => x.Role).HasMaxLength(64).IsRequired();
workspaceInvite.Property(x => x.Status).HasMaxLength(64).IsRequired();
workspaceInvite.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
workspaceInvite.HasIndex(x => x.WorkspaceId);
workspaceInvite.HasIndex(x => new { x.WorkspaceId, x.Email, x.Status });
});
modelBuilder.Entity<Client>(client =>
{
client.ToTable("Clients");
client.HasKey(x => x.Id);
client.Property(x => x.Name).HasMaxLength(256).IsRequired();
client.Property(x => x.Status).HasMaxLength(64).IsRequired();
client.Property(x => x.PortraitUrl).HasMaxLength(2048);
client.Property(x => x.PrimaryContactName).HasMaxLength(256);
client.Property(x => x.PrimaryContactEmail).HasMaxLength(256);
client.Property(x => x.PrimaryContactPortraitUrl).HasMaxLength(2048);
client.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
client.HasIndex(x => new { x.WorkspaceId, x.Name }).IsUnique();
client.HasIndex(x => x.WorkspaceId);
});
modelBuilder.Entity<Project>(project =>
{
project.ToTable("Projects");
project.HasKey(x => x.Id);
project.Property(x => x.Name).HasMaxLength(256).IsRequired();
project.Property(x => x.Description).HasMaxLength(4000);
project.Property(x => x.Notes).HasMaxLength(4000);
project.Property(x => x.Status).HasMaxLength(64).IsRequired();
project.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
project.HasIndex(x => new { x.ClientId, x.Name }).IsUnique();
project.HasIndex(x => x.WorkspaceId);
project.HasIndex(x => x.ClientId);
});
modelBuilder.Entity<ContentItem>(contentItem =>
{
contentItem.ToTable("ContentItems");
contentItem.HasKey(x => x.Id);
contentItem.Property(x => x.Title).HasMaxLength(256).IsRequired();
contentItem.Property(x => x.PublicationMessage).HasMaxLength(4000).IsRequired();
contentItem.Property(x => x.PublicationTargets).HasMaxLength(512).IsRequired();
contentItem.Property(x => x.Hashtags).HasMaxLength(1024);
contentItem.Property(x => x.Status).HasMaxLength(64).IsRequired();
contentItem.Property(x => x.CurrentRevisionLabel).HasMaxLength(32).IsRequired();
contentItem.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
contentItem.HasIndex(x => x.WorkspaceId);
contentItem.HasIndex(x => x.ClientId);
contentItem.HasIndex(x => x.ProjectId);
});
modelBuilder.Entity<ContentItemRevision>(revision =>
{
revision.ToTable("ContentItemRevisions");
revision.HasKey(x => x.Id);
revision.Property(x => x.RevisionLabel).HasMaxLength(32).IsRequired();
revision.Property(x => x.Title).HasMaxLength(256).IsRequired();
revision.Property(x => x.PublicationMessage).HasMaxLength(4000).IsRequired();
revision.Property(x => x.PublicationTargets).HasMaxLength(512).IsRequired();
revision.Property(x => x.Hashtags).HasMaxLength(1024);
revision.Property(x => x.ChangeSummary).HasMaxLength(1024);
revision.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
revision.HasIndex(x => x.ContentItemId);
revision.HasIndex(x => new { x.ContentItemId, x.RevisionNumber }).IsUnique();
});
modelBuilder.Entity<Asset>(asset =>
{
asset.ToTable("Assets");
asset.HasKey(x => x.Id);
asset.Property(x => x.AssetType).HasMaxLength(64).IsRequired();
asset.Property(x => x.SourceType).HasMaxLength(64).IsRequired();
asset.Property(x => x.DisplayName).HasMaxLength(256).IsRequired();
asset.Property(x => x.GoogleDriveFileId).HasMaxLength(256);
asset.Property(x => x.GoogleDriveLink).HasMaxLength(2048);
asset.Property(x => x.PreviewUrl).HasMaxLength(2048);
asset.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
asset.HasIndex(x => x.WorkspaceId);
asset.HasIndex(x => x.ContentItemId);
});
modelBuilder.Entity<AssetRevision>(revision =>
{
revision.ToTable("AssetRevisions");
revision.HasKey(x => x.Id);
revision.Property(x => x.SourceReference).HasMaxLength(2048).IsRequired();
revision.Property(x => x.PreviewUrl).HasMaxLength(2048);
revision.Property(x => x.Notes).HasMaxLength(1024);
revision.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
revision.HasIndex(x => x.AssetId);
revision.HasIndex(x => new { x.AssetId, x.RevisionNumber }).IsUnique();
});
modelBuilder.Entity<Comment>(comment =>
{
comment.ToTable("Comments");
comment.HasKey(x => x.Id);
comment.Property(x => x.AuthorDisplayName).HasMaxLength(256).IsRequired();
comment.Property(x => x.AuthorEmail).HasMaxLength(256).IsRequired();
comment.Property(x => x.Body).HasMaxLength(4000).IsRequired();
comment.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
comment.HasIndex(x => x.WorkspaceId);
comment.HasIndex(x => x.ContentItemId);
comment.HasIndex(x => x.ParentCommentId);
});
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
{
approvalRequest.ToTable("ApprovalRequests");
approvalRequest.HasKey(x => x.Id);
approvalRequest.Property(x => x.Stage).HasMaxLength(64).IsRequired();
approvalRequest.Property(x => x.ReviewerName).HasMaxLength(256).IsRequired();
approvalRequest.Property(x => x.ReviewerEmail).HasMaxLength(256).IsRequired();
approvalRequest.Property(x => x.State).HasMaxLength(64).IsRequired();
approvalRequest.Property(x => x.AccessToken).HasMaxLength(64).IsRequired();
approvalRequest.Property(x => x.SentAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
approvalRequest.HasIndex(x => x.WorkspaceId);
approvalRequest.HasIndex(x => x.ContentItemId);
approvalRequest.HasIndex(x => x.ReviewerEmail);
});
modelBuilder.Entity<ApprovalDecision>(approvalDecision =>
{
approvalDecision.ToTable("ApprovalDecisions");
approvalDecision.HasKey(x => x.Id);
approvalDecision.Property(x => x.Decision).HasMaxLength(64).IsRequired();
approvalDecision.Property(x => x.Comment).HasMaxLength(2048);
approvalDecision.Property(x => x.DecidedByName).HasMaxLength(256).IsRequired();
approvalDecision.Property(x => x.DecidedByEmail).HasMaxLength(256).IsRequired();
approvalDecision.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
approvalDecision.HasIndex(x => x.ApprovalRequestId);
});
modelBuilder.Entity<NotificationEvent>(notificationEvent =>
{
notificationEvent.ToTable("NotificationEvents");
notificationEvent.HasKey(x => x.Id);
notificationEvent.Property(x => x.EventType).HasMaxLength(128).IsRequired();
notificationEvent.Property(x => x.EntityType).HasMaxLength(128).IsRequired();
notificationEvent.Property(x => x.Message).HasMaxLength(1024).IsRequired();
notificationEvent.Property(x => x.RecipientEmail).HasMaxLength(256);
notificationEvent.Property(x => x.MetadataJson).HasMaxLength(4000);
notificationEvent.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
notificationEvent.HasIndex(x => x.WorkspaceId);
notificationEvent.HasIndex(x => x.ContentItemId);
notificationEvent.HasIndex(x => x.RecipientUserId);
notificationEvent.HasIndex(x => x.CreatedAt);
});
modelBuilder.ConfigureWorkspacesModule();
modelBuilder.ConfigureClientsModule();
modelBuilder.ConfigureProjectsModule();
modelBuilder.ConfigureContentItemsModule();
modelBuilder.ConfigureAssetsModule();
modelBuilder.ConfigureCommentsModule();
modelBuilder.ConfigureApprovalsModule();
modelBuilder.ConfigureNotificationsModule();
}
}

View File

@@ -0,0 +1,40 @@
namespace Socialize.Modules.Approvals.Data;
public static class ApprovalModelConfiguration
{
public static ModelBuilder ConfigureApprovalsModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
{
approvalRequest.ToTable("ApprovalRequests");
approvalRequest.HasKey(x => x.Id);
approvalRequest.Property(x => x.Stage).HasMaxLength(64).IsRequired();
approvalRequest.Property(x => x.ReviewerName).HasMaxLength(256).IsRequired();
approvalRequest.Property(x => x.ReviewerEmail).HasMaxLength(256).IsRequired();
approvalRequest.Property(x => x.State).HasMaxLength(64).IsRequired();
approvalRequest.Property(x => x.AccessToken).HasMaxLength(64).IsRequired();
approvalRequest.Property(x => x.SentAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
approvalRequest.HasIndex(x => x.WorkspaceId);
approvalRequest.HasIndex(x => x.ContentItemId);
approvalRequest.HasIndex(x => x.ReviewerEmail);
});
modelBuilder.Entity<ApprovalDecision>(approvalDecision =>
{
approvalDecision.ToTable("ApprovalDecisions");
approvalDecision.HasKey(x => x.Id);
approvalDecision.Property(x => x.Decision).HasMaxLength(64).IsRequired();
approvalDecision.Property(x => x.Comment).HasMaxLength(2048);
approvalDecision.Property(x => x.DecidedByName).HasMaxLength(256).IsRequired();
approvalDecision.Property(x => x.DecidedByEmail).HasMaxLength(256).IsRequired();
approvalDecision.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
approvalDecision.HasIndex(x => x.ApprovalRequestId);
});
return modelBuilder;
}
}

View File

@@ -0,0 +1,40 @@
namespace Socialize.Modules.Assets.Data;
public static class AssetModelConfiguration
{
public static ModelBuilder ConfigureAssetsModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<Asset>(asset =>
{
asset.ToTable("Assets");
asset.HasKey(x => x.Id);
asset.Property(x => x.AssetType).HasMaxLength(64).IsRequired();
asset.Property(x => x.SourceType).HasMaxLength(64).IsRequired();
asset.Property(x => x.DisplayName).HasMaxLength(256).IsRequired();
asset.Property(x => x.GoogleDriveFileId).HasMaxLength(256);
asset.Property(x => x.GoogleDriveLink).HasMaxLength(2048);
asset.Property(x => x.PreviewUrl).HasMaxLength(2048);
asset.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
asset.HasIndex(x => x.WorkspaceId);
asset.HasIndex(x => x.ContentItemId);
});
modelBuilder.Entity<AssetRevision>(revision =>
{
revision.ToTable("AssetRevisions");
revision.HasKey(x => x.Id);
revision.Property(x => x.SourceReference).HasMaxLength(2048).IsRequired();
revision.Property(x => x.PreviewUrl).HasMaxLength(2048);
revision.Property(x => x.Notes).HasMaxLength(1024);
revision.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
revision.HasIndex(x => x.AssetId);
revision.HasIndex(x => new { x.AssetId, x.RevisionNumber }).IsUnique();
});
return modelBuilder;
}
}

View File

@@ -0,0 +1,26 @@
namespace Socialize.Modules.Clients.Data;
public static class ClientModelConfiguration
{
public static ModelBuilder ConfigureClientsModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<Client>(client =>
{
client.ToTable("Clients");
client.HasKey(x => x.Id);
client.Property(x => x.Name).HasMaxLength(256).IsRequired();
client.Property(x => x.Status).HasMaxLength(64).IsRequired();
client.Property(x => x.PortraitUrl).HasMaxLength(2048);
client.Property(x => x.PrimaryContactName).HasMaxLength(256);
client.Property(x => x.PrimaryContactEmail).HasMaxLength(256);
client.Property(x => x.PrimaryContactPortraitUrl).HasMaxLength(2048);
client.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
client.HasIndex(x => new { x.WorkspaceId, x.Name }).IsUnique();
client.HasIndex(x => x.WorkspaceId);
});
return modelBuilder;
}
}

View File

@@ -0,0 +1,24 @@
namespace Socialize.Modules.Comments.Data;
public static class CommentModelConfiguration
{
public static ModelBuilder ConfigureCommentsModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<Comment>(comment =>
{
comment.ToTable("Comments");
comment.HasKey(x => x.Id);
comment.Property(x => x.AuthorDisplayName).HasMaxLength(256).IsRequired();
comment.Property(x => x.AuthorEmail).HasMaxLength(256).IsRequired();
comment.Property(x => x.Body).HasMaxLength(4000).IsRequired();
comment.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
comment.HasIndex(x => x.WorkspaceId);
comment.HasIndex(x => x.ContentItemId);
comment.HasIndex(x => x.ParentCommentId);
});
return modelBuilder;
}
}

View File

@@ -0,0 +1,44 @@
namespace Socialize.Modules.ContentItems.Data;
public static class ContentItemModelConfiguration
{
public static ModelBuilder ConfigureContentItemsModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<ContentItem>(contentItem =>
{
contentItem.ToTable("ContentItems");
contentItem.HasKey(x => x.Id);
contentItem.Property(x => x.Title).HasMaxLength(256).IsRequired();
contentItem.Property(x => x.PublicationMessage).HasMaxLength(4000).IsRequired();
contentItem.Property(x => x.PublicationTargets).HasMaxLength(512).IsRequired();
contentItem.Property(x => x.Hashtags).HasMaxLength(1024);
contentItem.Property(x => x.Status).HasMaxLength(64).IsRequired();
contentItem.Property(x => x.CurrentRevisionLabel).HasMaxLength(32).IsRequired();
contentItem.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
contentItem.HasIndex(x => x.WorkspaceId);
contentItem.HasIndex(x => x.ClientId);
contentItem.HasIndex(x => x.ProjectId);
});
modelBuilder.Entity<ContentItemRevision>(revision =>
{
revision.ToTable("ContentItemRevisions");
revision.HasKey(x => x.Id);
revision.Property(x => x.RevisionLabel).HasMaxLength(32).IsRequired();
revision.Property(x => x.Title).HasMaxLength(256).IsRequired();
revision.Property(x => x.PublicationMessage).HasMaxLength(4000).IsRequired();
revision.Property(x => x.PublicationTargets).HasMaxLength(512).IsRequired();
revision.Property(x => x.Hashtags).HasMaxLength(1024);
revision.Property(x => x.ChangeSummary).HasMaxLength(1024);
revision.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
revision.HasIndex(x => x.ContentItemId);
revision.HasIndex(x => new { x.ContentItemId, x.RevisionNumber }).IsUnique();
});
return modelBuilder;
}
}

View File

@@ -0,0 +1,27 @@
namespace Socialize.Modules.Notifications.Data;
public static class NotificationModelConfiguration
{
public static ModelBuilder ConfigureNotificationsModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<NotificationEvent>(notificationEvent =>
{
notificationEvent.ToTable("NotificationEvents");
notificationEvent.HasKey(x => x.Id);
notificationEvent.Property(x => x.EventType).HasMaxLength(128).IsRequired();
notificationEvent.Property(x => x.EntityType).HasMaxLength(128).IsRequired();
notificationEvent.Property(x => x.Message).HasMaxLength(1024).IsRequired();
notificationEvent.Property(x => x.RecipientEmail).HasMaxLength(256);
notificationEvent.Property(x => x.MetadataJson).HasMaxLength(4000);
notificationEvent.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
notificationEvent.HasIndex(x => x.WorkspaceId);
notificationEvent.HasIndex(x => x.ContentItemId);
notificationEvent.HasIndex(x => x.RecipientUserId);
notificationEvent.HasIndex(x => x.CreatedAt);
});
return modelBuilder;
}
}

View File

@@ -0,0 +1,25 @@
namespace Socialize.Modules.Projects.Data;
public static class ProjectModelConfiguration
{
public static ModelBuilder ConfigureProjectsModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<Project>(project =>
{
project.ToTable("Projects");
project.HasKey(x => x.Id);
project.Property(x => x.Name).HasMaxLength(256).IsRequired();
project.Property(x => x.Description).HasMaxLength(4000);
project.Property(x => x.Notes).HasMaxLength(4000);
project.Property(x => x.Status).HasMaxLength(64).IsRequired();
project.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
project.HasIndex(x => new { x.ClientId, x.Name }).IsUnique();
project.HasIndex(x => x.WorkspaceId);
project.HasIndex(x => x.ClientId);
});
return modelBuilder;
}
}

View File

@@ -0,0 +1,37 @@
namespace Socialize.Modules.Workspaces.Data;
public static class WorkspaceModelConfiguration
{
public static ModelBuilder ConfigureWorkspacesModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<Workspace>(workspace =>
{
workspace.ToTable("Workspaces");
workspace.HasKey(x => x.Id);
workspace.Property(x => x.Name).HasMaxLength(256).IsRequired();
workspace.Property(x => x.Slug).HasMaxLength(128).IsRequired();
workspace.Property(x => x.TimeZone).HasMaxLength(128).IsRequired();
workspace.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
workspace.HasIndex(x => x.Slug).IsUnique();
workspace.HasIndex(x => x.OwnerUserId);
});
modelBuilder.Entity<WorkspaceInvite>(workspaceInvite =>
{
workspaceInvite.ToTable("WorkspaceInvites");
workspaceInvite.HasKey(x => x.Id);
workspaceInvite.Property(x => x.Email).HasMaxLength(256).IsRequired();
workspaceInvite.Property(x => x.Role).HasMaxLength(64).IsRequired();
workspaceInvite.Property(x => x.Status).HasMaxLength(64).IsRequired();
workspaceInvite.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
workspaceInvite.HasIndex(x => x.WorkspaceId);
workspaceInvite.HasIndex(x => new { x.WorkspaceId, x.Email, x.Status });
});
return modelBuilder;
}
}

View File

@@ -0,0 +1,37 @@
# Task: Contain backend feature mapping
## Feature
`docs/FEATURES/platform-scaffold.md`
## Goal
Move backend feature-specific persistence mapping out of the shared `AppDbContext` body and into the owning `Modules/<Feature>/Data` folders.
## Context
Architecture docs state that current backend feature code stays under `Modules/<Feature>`. `AppDbContext` remains the shared EF Core composition point, but feature-owned model configuration should live with the feature entities.
## Files Likely To Change
- `backend/src/Socialize.Api/Data/AppDbContext.cs`
- `backend/src/Socialize.Api/Modules/*/Data/*`
## Constraints
- Do not change API contracts.
- Do not change table names, indexes, or column constraints.
- Do not introduce a broader persistence refactor.
## Done When
- [x] Feature entity mappings live under the owning module folders.
- [x] `AppDbContext` delegates feature configuration to modules.
- [x] Backend build passes.
## Validation Commands
```bash
dotnet build backend/Socialize.slnx
dotnet test backend/Socialize.slnx
```