ref: project structure
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
using mws.backend.dotnet.domain.MasterData;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
using mws.backend.dotnet.domain.Tasks;
|
||||
using mws.backend.dotnet.domain.Users;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence;
|
||||
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<UserRole> UserRoles => Set<UserRole>();
|
||||
public DbSet<Project> Projects => Set<Project>();
|
||||
public DbSet<ProjectMember> ProjectMembers => Set<ProjectMember>();
|
||||
public DbSet<ProjectMemberPermission> ProjectMemberPermissions => Set<ProjectMemberPermission>();
|
||||
public DbSet<Document> Documents => Set<Document>();
|
||||
public DbSet<TaskItem> Tasks => Set<TaskItem>();
|
||||
public DbSet<Role> Roles => Set<Role>();
|
||||
public DbSet<RolePermission> RolePermissions => Set<RolePermission>();
|
||||
public DbSet<MasterDataEntry> MasterData => Set<MasterDataEntry>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||
|
||||
public class DocumentConfiguration : IEntityTypeConfiguration<Document>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Document> e)
|
||||
{
|
||||
e.ToTable("documents");
|
||||
e.HasKey(d => d.Id);
|
||||
e.Property(d => d.Title).HasMaxLength(300).IsRequired();
|
||||
e.Property(d => d.Type).HasConversion<string>().HasMaxLength(20);
|
||||
e.Property(d => d.Content).HasColumnType("text");
|
||||
|
||||
e.HasIndex(d => new { d.ProjectId, d.ParentId });
|
||||
e.HasIndex(d => d.ParentId);
|
||||
|
||||
e.HasOne(d => d.Parent)
|
||||
.WithMany(d => d.Children)
|
||||
.HasForeignKey(d => d.ParentId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using mws.backend.dotnet.domain.MasterData;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||
|
||||
public class MasterDataEntryConfiguration : IEntityTypeConfiguration<MasterDataEntry>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<MasterDataEntry> e)
|
||||
{
|
||||
e.ToTable("master_data");
|
||||
e.HasKey(m => m.Id);
|
||||
e.Property(m => m.Group).HasMaxLength(100).IsRequired();
|
||||
e.Property(m => m.Label).HasMaxLength(200).IsRequired();
|
||||
e.Property(m => m.Value).HasMaxLength(100).IsRequired();
|
||||
e.HasIndex(m => new { m.Group, m.Value }).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||
|
||||
public class ProjectConfiguration : IEntityTypeConfiguration<Project>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Project> e)
|
||||
{
|
||||
e.ToTable("projects");
|
||||
e.HasKey(p => p.Id);
|
||||
e.Property(p => p.Name).HasMaxLength(200).IsRequired();
|
||||
e.Property(p => p.Description).HasMaxLength(2000);
|
||||
e.Property(p => p.Status).HasConversion<string>().HasMaxLength(20);
|
||||
|
||||
e.HasIndex(p => p.CreatedBy);
|
||||
e.HasIndex(p => p.UpdatedBy);
|
||||
|
||||
e.HasOne(p => p.CreatedByUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.CreatedBy)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasOne(p => p.UpdatedByUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.UpdatedBy)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||
|
||||
public class ProjectMemberConfiguration : IEntityTypeConfiguration<ProjectMember>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ProjectMember> e)
|
||||
{
|
||||
e.ToTable("project_members");
|
||||
e.HasKey(m => new { m.ProjectId, m.UserId });
|
||||
e.Property(m => m.Role).HasConversion<string>().HasMaxLength(20);
|
||||
|
||||
e.HasOne(m => m.Project)
|
||||
.WithMany(p => p.Members)
|
||||
.HasForeignKey(m => m.ProjectId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
e.HasOne(m => m.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||
|
||||
public class ProjectMemberPermissionConfiguration : IEntityTypeConfiguration<ProjectMemberPermission>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ProjectMemberPermission> e)
|
||||
{
|
||||
e.ToTable("project_member_permissions");
|
||||
e.HasKey(p => new { p.ProjectId, p.UserId, p.Screen });
|
||||
e.Property(p => p.Screen).HasMaxLength(50).IsRequired();
|
||||
|
||||
e.HasOne<ProjectMember>()
|
||||
.WithMany()
|
||||
.HasForeignKey(p => new { p.ProjectId, p.UserId })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||
|
||||
public class RoleConfiguration : IEntityTypeConfiguration<Role>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Role> e)
|
||||
{
|
||||
e.ToTable("roles");
|
||||
e.HasKey(r => r.Id);
|
||||
e.Property(r => r.Name).HasMaxLength(100).IsRequired();
|
||||
e.HasIndex(r => r.Name).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||
|
||||
public class RolePermissionConfiguration : IEntityTypeConfiguration<RolePermission>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<RolePermission> e)
|
||||
{
|
||||
e.ToTable("role_permissions");
|
||||
e.HasKey(p => new { p.RoleId, p.Screen });
|
||||
e.Property(p => p.Screen).HasMaxLength(50).IsRequired();
|
||||
|
||||
e.HasOne(p => p.Role)
|
||||
.WithMany(r => r.Permissions)
|
||||
.HasForeignKey(p => p.RoleId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using mws.backend.dotnet.domain.Tasks;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||
|
||||
public class TaskConfiguration : IEntityTypeConfiguration<TaskItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TaskItem> e)
|
||||
{
|
||||
e.ToTable("tasks");
|
||||
e.HasKey(t => t.Id);
|
||||
e.Property(t => t.Title).HasMaxLength(300).IsRequired();
|
||||
e.Property(t => t.Description).HasColumnType("text");
|
||||
e.Property(t => t.Status).HasConversion<string>().HasMaxLength(20);
|
||||
e.Property(t => t.Priority).HasConversion<string>().HasMaxLength(20);
|
||||
|
||||
e.HasIndex(t => t.ProjectId);
|
||||
e.HasIndex(t => t.AssigneeId);
|
||||
e.HasIndex(t => t.Status);
|
||||
|
||||
e.HasOne(t => t.Assignee)
|
||||
.WithMany()
|
||||
.HasForeignKey(t => t.AssigneeId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using mws.backend.dotnet.domain.Users;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||
|
||||
public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> e)
|
||||
{
|
||||
e.ToTable("users");
|
||||
e.HasKey(u => u.Id);
|
||||
e.Property(u => u.Username).HasMaxLength(100).IsRequired();
|
||||
e.HasIndex(u => u.Username).IsUnique();
|
||||
e.Property(u => u.PasswordHash).HasMaxLength(200).IsRequired();
|
||||
e.Property(u => u.DisplayName).HasMaxLength(150).IsRequired();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using mws.backend.dotnet.domain.Users;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||
|
||||
public class UserRoleConfiguration : IEntityTypeConfiguration<UserRole>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserRole> e)
|
||||
{
|
||||
e.ToTable("user_roles");
|
||||
e.HasKey(ur => new { ur.UserId, ur.RoleId });
|
||||
|
||||
e.HasOne(ur => ur.User)
|
||||
.WithMany(u => u.UserRoles)
|
||||
.HasForeignKey(ur => ur.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
e.HasOne(ur => ur.Role)
|
||||
.WithMany(r => r.UserRoles)
|
||||
.HasForeignKey(ur => ur.RoleId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using mws.backend.dotnet.application.Auth;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
using mws.backend.dotnet.domain.MasterData;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
using mws.backend.dotnet.domain.Tasks;
|
||||
using mws.backend.dotnet.domain.Users;
|
||||
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
|
||||
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence;
|
||||
|
||||
public static class DbSeeder
|
||||
{
|
||||
public static async Task SeedAsync(AppDbContext db, IPasswordHasher passwordHasher)
|
||||
{
|
||||
await SeedMasterDataAsync(db);
|
||||
await EnsureRolesAsync(db);
|
||||
|
||||
var admin = await EnsureUserAsync(db, passwordHasher, "admin", "Admin", "Admin");
|
||||
var alice = await EnsureUserAsync(db, passwordHasher, "alice", "Alice", "Member");
|
||||
var bob = await EnsureUserAsync(db, passwordHasher, "bob", "Bob", "Member");
|
||||
await EnsureUserAsync(db, passwordHasher, "namdh", "Nam DH", "Member");
|
||||
|
||||
if (await db.Projects.AnyAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var project = new Project
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "MWS",
|
||||
Description = "My Workspace - internal tool for the team.",
|
||||
Status = ProjectStatus.Active,
|
||||
CreatedBy = admin.Id,
|
||||
CreatedAt = now,
|
||||
UpdatedBy = admin.Id,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
db.Projects.Add(project);
|
||||
|
||||
db.ProjectMembers.AddRange(
|
||||
new ProjectMember { ProjectId = project.Id, UserId = admin.Id, Role = MemberRole.Owner },
|
||||
new ProjectMember { ProjectId = project.Id, UserId = alice.Id, Role = MemberRole.Member },
|
||||
new ProjectMember { ProjectId = project.Id, UserId = bob.Id, Role = MemberRole.Member });
|
||||
|
||||
db.ProjectMemberPermissions.AddRange(
|
||||
new ProjectMemberPermission { ProjectId = project.Id, UserId = alice.Id, Screen = ProjectPermissionScreens.Documents, CanView = true },
|
||||
new ProjectMemberPermission { ProjectId = project.Id, UserId = bob.Id, Screen = ProjectPermissionScreens.Documents, CanView = true });
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var requirements = new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
|
||||
Title = "Requirements", Type = DocumentType.Folder,
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
};
|
||||
|
||||
var design = new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
|
||||
Title = "Technical Design", Type = DocumentType.Folder,
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
};
|
||||
|
||||
var notes = new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
|
||||
Title = "Notes", Type = DocumentType.Folder,
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
};
|
||||
|
||||
db.Documents.AddRange(requirements, design, notes);
|
||||
db.Documents.Add(new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = requirements.Id,
|
||||
Title = "Authentication", Type = DocumentType.Document,
|
||||
Content = "<h2>Authentication</h2><p>Phase 1 uses simple username/password with JWT.</p>",
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
});
|
||||
db.Documents.Add(new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = requirements.Id,
|
||||
Title = "User Management", Type = DocumentType.Document,
|
||||
Content = "<h2>User Management</h2><p>Users are created via seeding in Phase 1.</p>",
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
});
|
||||
db.Documents.Add(new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = design.Id,
|
||||
Title = "API Design", Type = DocumentType.Document,
|
||||
Content = "<h2>API Design</h2><p>REST API with ASP.NET Core Web API.</p>",
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
db.Tasks.AddRange(
|
||||
new TaskItem
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Set up solution",
|
||||
Status = TaskStatus.Done, Priority = TaskPriority.High, AssigneeId = admin.Id,
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
|
||||
},
|
||||
new TaskItem
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Implement authentication",
|
||||
Description = "Login endpoint with BCrypt and JWT.",
|
||||
Status = TaskStatus.InProgress, Priority = TaskPriority.High, AssigneeId = alice.Id,
|
||||
DueDate = now.AddDays(3),
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
|
||||
},
|
||||
new TaskItem
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Task board drag & drop",
|
||||
Status = TaskStatus.Todo, Priority = TaskPriority.Medium, AssigneeId = bob.Id,
|
||||
DueDate = now.AddDays(7),
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
|
||||
},
|
||||
new TaskItem
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Polish documents UI",
|
||||
Status = TaskStatus.Todo, Priority = TaskPriority.Low,
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static async Task EnsureRolesAsync(AppDbContext db)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (!await db.Roles.AnyAsync(r => r.Name == "Admin"))
|
||||
{
|
||||
var adminRole = new Role { Id = Guid.NewGuid(), Name = "Admin", IsSystem = true, CreatedAt = now, UpdatedAt = now };
|
||||
adminRole.Permissions = ScreenCatalog.Screens.Select(s => new RolePermission
|
||||
{
|
||||
RoleId = adminRole.Id, Screen = s.Key, CanView = true, CanCreate = true, CanEdit = true, CanDelete = true,
|
||||
}).ToList();
|
||||
db.Roles.Add(adminRole);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (!await db.Roles.AnyAsync(r => r.Name == "Member"))
|
||||
{
|
||||
var memberRole = new Role { Id = Guid.NewGuid(), Name = "Member", IsSystem = true, CreatedAt = now, UpdatedAt = now };
|
||||
memberRole.Permissions = ScreenCatalog.Screens
|
||||
.Where(s => s.Key is not ("users" or "permissions" or "masterdata"))
|
||||
.Select(s => new RolePermission
|
||||
{
|
||||
RoleId = memberRole.Id, Screen = s.Key, CanView = true, CanCreate = true, CanEdit = true, CanDelete = false,
|
||||
}).ToList();
|
||||
db.Roles.Add(memberRole);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<User> EnsureUserAsync(AppDbContext db, IPasswordHasher passwordHasher, string username, string displayName, string roleName)
|
||||
{
|
||||
var existing = await db.Users.Include(u => u.UserRoles).FirstOrDefaultAsync(u => u.Username == username);
|
||||
if (existing != null)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var role = await db.Roles.FirstAsync(r => r.Name == roleName);
|
||||
var now = DateTime.UtcNow;
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = username,
|
||||
PasswordHash = passwordHasher.Hash("password"),
|
||||
DisplayName = displayName,
|
||||
UserRoles = new List<UserRole> { new UserRole { RoleId = role.Id } },
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
db.Users.Add(user);
|
||||
await db.SaveChangesAsync();
|
||||
return user;
|
||||
}
|
||||
|
||||
private static async Task SeedMasterDataAsync(AppDbContext db)
|
||||
{
|
||||
if (await db.MasterData.AnyAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
MasterDataEntry Entry(string group, string label, string value, int sortOrder) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(), Group = group, Label = label, Value = value, SortOrder = sortOrder,
|
||||
IsActive = true, CreatedAt = now, UpdatedAt = now,
|
||||
};
|
||||
|
||||
db.MasterData.AddRange(
|
||||
Entry("task_status", "Todo", nameof(TaskStatus.Todo), 1),
|
||||
Entry("task_status", "In Progress", nameof(TaskStatus.InProgress), 2),
|
||||
Entry("task_status", "Done", nameof(TaskStatus.Done), 3),
|
||||
Entry("task_status", "Cancelled", nameof(TaskStatus.Cancelled), 4),
|
||||
Entry("task_priority", "Low", nameof(TaskPriority.Low), 1),
|
||||
Entry("task_priority", "Medium", nameof(TaskPriority.Medium), 2),
|
||||
Entry("task_priority", "High", nameof(TaskPriority.High), 3),
|
||||
Entry("project_status", "Active", nameof(ProjectStatus.Active), 1),
|
||||
Entry("project_status", "Archived", nameof(ProjectStatus.Archived), 2),
|
||||
Entry("member_role", "Owner", nameof(MemberRole.Owner), 1),
|
||||
Entry("member_role", "Member", nameof(MemberRole.Member), 2),
|
||||
Entry("document_type", "Folder", nameof(DocumentType.Folder), 1),
|
||||
Entry("document_type", "Document", nameof(DocumentType.Document), 2));
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence;
|
||||
|
||||
public static class PaginationExtensions
|
||||
{
|
||||
public const int DefaultPageSize = 20;
|
||||
public const int MaxPageSize = 100;
|
||||
|
||||
public static async Task<PagedResult<T>> ToPagedResultAsync<T>(
|
||||
this IQueryable<T> query, int page, int pageSize, CancellationToken ct = default)
|
||||
{
|
||||
page = page < 1 ? 1 : page;
|
||||
pageSize = pageSize < 1 ? DefaultPageSize : Math.Min(pageSize, MaxPageSize);
|
||||
|
||||
var totalCount = await query.CountAsync(ct);
|
||||
var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(ct);
|
||||
|
||||
return new PagedResult<T> { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using mws.backend.dotnet.application.Common.Repositories;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
|
||||
|
||||
public class DocumentRepository(AppDbContext db) : RepositoryBase<Document>(db), IDocumentRepository
|
||||
{
|
||||
public Task<Document?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(d => d.Id == id, ct);
|
||||
|
||||
public Task<List<Document>> GetTreeForProjectAsync(Guid projectId, CancellationToken ct = default) =>
|
||||
Set.Where(d => d.ProjectId == projectId).OrderBy(d => d.Title).ToListAsync(ct);
|
||||
|
||||
public Task<Document?> GetInProjectAsync(Guid parentId, Guid projectId, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(d => d.Id == parentId && d.ProjectId == projectId, ct);
|
||||
|
||||
public Task<Guid?> GetParentIdAsync(Guid documentId, CancellationToken ct = default) =>
|
||||
Set.Where(d => d.Id == documentId).Select(d => d.ParentId).SingleAsync(ct);
|
||||
|
||||
public Task<List<Document>> GetChildrenAsync(Guid parentId, CancellationToken ct = default) =>
|
||||
Set.Where(d => d.ParentId == parentId).ToListAsync(ct);
|
||||
|
||||
public Task<List<Document>> SearchAsync(List<Guid> projectIds, string? term, int take, CancellationToken ct = default)
|
||||
{
|
||||
var query = Set.Where(d => projectIds.Contains(d.ProjectId));
|
||||
if (!string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
var lower = term.Trim().ToLower();
|
||||
query = query.Where(d => d.Title.ToLower().Contains(lower));
|
||||
}
|
||||
|
||||
return query.OrderByDescending(d => d.UpdatedAt).Take(take).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public Task<int> CountByTypeForProjectAsync(Guid projectId, DocumentType type, CancellationToken ct = default) =>
|
||||
Set.CountAsync(d => d.ProjectId == projectId && d.Type == type, ct);
|
||||
|
||||
public Task<List<Document>> GetRecentForProjectAsync(Guid projectId, DocumentType type, int take, CancellationToken ct = default) =>
|
||||
Set.Where(d => d.ProjectId == projectId && d.Type == type).OrderByDescending(d => d.UpdatedAt).Take(take).ToListAsync(ct);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Common.Repositories;
|
||||
using mws.backend.dotnet.domain.MasterData;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
|
||||
|
||||
public class MasterDataRepository(AppDbContext db) : RepositoryBase<MasterDataEntry>(db), IMasterDataRepository
|
||||
{
|
||||
public Task<PagedResult<MasterDataEntry>> GetAllAsync(string? group, int page, int pageSize, CancellationToken ct = default) =>
|
||||
Set.Where(m => group == null || m.Group == group)
|
||||
.OrderBy(m => m.Group).ThenBy(m => m.SortOrder).ThenBy(m => m.Label)
|
||||
.ToPagedResultAsync(page, pageSize, ct);
|
||||
|
||||
public Task<List<MasterDataEntry>> GetActiveByGroupAsync(string group, CancellationToken ct = default) =>
|
||||
Set.Where(m => m.Group == group && m.IsActive)
|
||||
.OrderBy(m => m.SortOrder).ThenBy(m => m.Label)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public Task<MasterDataEntry?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(m => m.Id == id, ct);
|
||||
|
||||
public Task<bool> ExistsAsync(string group, string value, Guid? excludeId, CancellationToken ct = default) =>
|
||||
Set.AnyAsync(m => m.Group == group && m.Value == value && (excludeId == null || m.Id != excludeId), ct);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Common.Repositories;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
|
||||
|
||||
public class ProjectRepository(AppDbContext db) : RepositoryBase<Project>(db), IProjectRepository
|
||||
{
|
||||
public Task<Project?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(p => p.Id == id, ct);
|
||||
|
||||
public Task<Project?> GetForUserAsync(Guid userId, Guid projectId, CancellationToken ct = default) =>
|
||||
Set.Include(p => p.CreatedByUser).Include(p => p.UpdatedByUser)
|
||||
.FirstOrDefaultAsync(p => p.Id == projectId && p.Members.Any(m => m.UserId == userId), ct);
|
||||
|
||||
public Task<PagedResult<Project>> GetForUserAsync(Guid userId, int page, int pageSize, CancellationToken ct = default) =>
|
||||
Set.Include(p => p.CreatedByUser).Include(p => p.UpdatedByUser)
|
||||
.Where(p => p.Members.Any(m => m.UserId == userId))
|
||||
.OrderByDescending(p => p.UpdatedAt)
|
||||
.ToPagedResultAsync(page, pageSize, ct);
|
||||
|
||||
public Task<List<Project>> SearchForUserAsync(Guid userId, string? term, CancellationToken ct = default)
|
||||
{
|
||||
var query = Set.Include(p => p.CreatedByUser).Include(p => p.UpdatedByUser)
|
||||
.Where(p => p.Members.Any(m => m.UserId == userId));
|
||||
if (!string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
var lower = term.Trim().ToLower();
|
||||
query = query.Where(p => p.Name.ToLower().Contains(lower));
|
||||
}
|
||||
|
||||
return query.OrderByDescending(p => p.UpdatedAt).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public Task<int> CountMembersAsync(Guid projectId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.CountAsync(m => m.ProjectId == projectId, ct);
|
||||
|
||||
public Task<bool> IsMemberAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.AnyAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
|
||||
|
||||
public Task<MemberRole?> GetMemberRoleAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers
|
||||
.Where(m => m.ProjectId == projectId && m.UserId == userId)
|
||||
.Select(m => (MemberRole?)m.Role)
|
||||
.SingleOrDefaultAsync(ct);
|
||||
|
||||
public Task<ProjectMember?> GetMemberAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.FirstOrDefaultAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
|
||||
|
||||
public Task<ProjectMember?> GetMemberWithUserAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.Include(m => m.User).FirstOrDefaultAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
|
||||
|
||||
public Task<PagedResult<ProjectMember>> GetMembersWithUserAsync(Guid projectId, int page, int pageSize, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers
|
||||
.Where(m => m.ProjectId == projectId)
|
||||
.Include(m => m.User)
|
||||
.OrderByDescending(m => m.Role)
|
||||
.ThenBy(m => m.User.DisplayName)
|
||||
.ToPagedResultAsync(page, pageSize, ct);
|
||||
|
||||
public Task<int> CountOwnersAsync(Guid projectId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.CountAsync(m => m.ProjectId == projectId && m.Role == MemberRole.Owner, ct);
|
||||
|
||||
public Task<List<Guid>> GetProjectIdsForUserAsync(Guid userId, CancellationToken ct = default) =>
|
||||
Set.Where(p => p.Members.Any(m => m.UserId == userId)).Select(p => p.Id).ToListAsync(ct);
|
||||
|
||||
public Task<List<Guid>> GetOwnedProjectIdsAsync(Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.Where(m => m.UserId == userId && m.Role == MemberRole.Owner).Select(m => m.ProjectId).ToListAsync(ct);
|
||||
|
||||
public Task<List<Guid>> GetDocumentViewableProjectIdsAsync(Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers
|
||||
.Where(m => m.UserId == userId &&
|
||||
(m.Role == MemberRole.Owner ||
|
||||
Db.ProjectMemberPermissions.Any(p => p.ProjectId == m.ProjectId && p.UserId == userId && p.Screen == ProjectPermissionScreens.Documents && p.CanView)))
|
||||
.Select(m => m.ProjectId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public void AddMember(ProjectMember member) => Db.ProjectMembers.Add(member);
|
||||
|
||||
public void RemoveMember(ProjectMember member) => Db.ProjectMembers.Remove(member);
|
||||
|
||||
public Task<ProjectMemberPermission?> GetMemberPermissionAsync(Guid projectId, Guid userId, string screen, CancellationToken ct = default) =>
|
||||
Db.ProjectMemberPermissions.FirstOrDefaultAsync(p => p.ProjectId == projectId && p.UserId == userId && p.Screen == screen, ct);
|
||||
|
||||
public Task<Dictionary<Guid, ProjectMemberPermission>> GetMemberPermissionsAsync(Guid projectId, string screen, CancellationToken ct = default) =>
|
||||
Db.ProjectMemberPermissions
|
||||
.Where(p => p.ProjectId == projectId && p.Screen == screen)
|
||||
.ToDictionaryAsync(p => p.UserId, ct);
|
||||
|
||||
public void AddMemberPermission(ProjectMemberPermission permission) => Db.ProjectMemberPermissions.Add(permission);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using mws.backend.dotnet.application.Common.Repositories;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
|
||||
|
||||
public abstract class RepositoryBase<T>(AppDbContext db) : IRepository<T> where T : class
|
||||
{
|
||||
protected readonly AppDbContext Db = db;
|
||||
protected DbSet<T> Set => Db.Set<T>();
|
||||
|
||||
public void Add(T entity) => Set.Add(entity);
|
||||
public void Remove(T entity) => Set.Remove(entity);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Common.Repositories;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
|
||||
|
||||
public class RoleRepository(AppDbContext db) : RepositoryBase<Role>(db), IRoleRepository
|
||||
{
|
||||
public Task<PagedResult<Role>> GetAllWithPermissionsAsync(int page, int pageSize, CancellationToken ct = default) =>
|
||||
Set.Include(r => r.Permissions).OrderBy(r => r.Name).ToPagedResultAsync(page, pageSize, ct);
|
||||
|
||||
public Task<Role?> GetByIdWithPermissionsAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.Include(r => r.Permissions).FirstOrDefaultAsync(r => r.Id == id, ct);
|
||||
|
||||
public Task<Role?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(r => r.Id == id, ct);
|
||||
|
||||
public Task<bool> ExistsByNameAsync(string name, Guid? excludeId, CancellationToken ct = default) =>
|
||||
Set.AnyAsync(r => r.Name == name && (excludeId == null || r.Id != excludeId), ct);
|
||||
|
||||
public Task<RolePermission?> GetPermissionAsync(Guid roleId, string screen, CancellationToken ct = default) =>
|
||||
Db.RolePermissions.FirstOrDefaultAsync(p => p.RoleId == roleId && p.Screen == screen, ct);
|
||||
|
||||
public Task<Dictionary<string, RolePermission>> GetPermissionsAsync(Guid roleId, CancellationToken ct = default) =>
|
||||
Db.RolePermissions.Where(p => p.RoleId == roleId).ToDictionaryAsync(p => p.Screen, ct);
|
||||
|
||||
public Task<List<RolePermission>> GetPermissionsForRolesAsync(IEnumerable<Guid> roleIds, CancellationToken ct = default) =>
|
||||
Db.RolePermissions.Where(p => roleIds.Contains(p.RoleId)).ToListAsync(ct);
|
||||
|
||||
public void RemovePermissions(IEnumerable<RolePermission> permissions) => Db.RolePermissions.RemoveRange(permissions);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Common.Repositories;
|
||||
using mws.backend.dotnet.domain.Tasks;
|
||||
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
|
||||
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
|
||||
|
||||
public class TaskRepository(AppDbContext db) : RepositoryBase<TaskItem>(db), ITaskRepository
|
||||
{
|
||||
public Task<TaskItem?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(t => t.Id == id, ct);
|
||||
|
||||
public Task<TaskItem?> GetWithAssigneeAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.Include(t => t.Assignee).FirstOrDefaultAsync(t => t.Id == id, ct);
|
||||
|
||||
public Task<PagedResult<TaskItem>> GetForProjectAsync(
|
||||
Guid projectId, TaskStatus? status, TaskPriority? priority, Guid? assigneeId, int page, int pageSize, CancellationToken ct = default)
|
||||
{
|
||||
var query = Set.Where(t => t.ProjectId == projectId);
|
||||
|
||||
if (status.HasValue)
|
||||
{
|
||||
query = query.Where(t => t.Status == status);
|
||||
}
|
||||
if (priority.HasValue)
|
||||
{
|
||||
query = query.Where(t => t.Priority == priority);
|
||||
}
|
||||
if (assigneeId.HasValue)
|
||||
{
|
||||
query = query.Where(t => t.AssigneeId == assigneeId);
|
||||
}
|
||||
|
||||
return query.Include(t => t.Assignee).OrderByDescending(t => t.UpdatedAt).ToPagedResultAsync(page, pageSize, ct);
|
||||
}
|
||||
|
||||
public Task<List<TaskItem>> SearchWithAssigneeAsync(List<Guid> projectIds, string? term, int take, CancellationToken ct = default)
|
||||
{
|
||||
var query = Set.Where(t => projectIds.Contains(t.ProjectId));
|
||||
if (!string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
var lower = term.Trim().ToLower();
|
||||
query = query.Where(t => t.Title.ToLower().Contains(lower));
|
||||
}
|
||||
|
||||
return query.Include(t => t.Assignee).OrderByDescending(t => t.UpdatedAt).Take(take).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<TaskStatus, int>> CountByStatusForProjectAsync(Guid projectId, CancellationToken ct = default)
|
||||
{
|
||||
return await Set.Where(t => t.ProjectId == projectId)
|
||||
.GroupBy(t => t.Status)
|
||||
.Select(g => new { Status = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(a => a.Status, a => a.Count, ct);
|
||||
}
|
||||
|
||||
public Task<List<TaskItem>> GetRecentForProjectAsync(Guid projectId, int take, CancellationToken ct = default) =>
|
||||
Set.Where(t => t.ProjectId == projectId).OrderByDescending(t => t.UpdatedAt).Take(take).ToListAsync(ct);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Common.Repositories;
|
||||
using mws.backend.dotnet.domain.Users;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
|
||||
|
||||
public class UserRepository(AppDbContext db) : RepositoryBase<User>(db), IUserRepository
|
||||
{
|
||||
public Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(u => u.Id == id, ct);
|
||||
|
||||
public Task<User?> GetByIdWithRoleAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.Include(u => u.UserRoles).ThenInclude(ur => ur.Role).FirstOrDefaultAsync(u => u.Id == id, ct);
|
||||
|
||||
public Task<User?> GetByUsernameWithRoleAsync(string username, CancellationToken ct = default) =>
|
||||
Set.Include(u => u.UserRoles).ThenInclude(ur => ur.Role).SingleOrDefaultAsync(u => u.Username == username, ct);
|
||||
|
||||
public Task<bool> ExistsByUsernameAsync(string username, CancellationToken ct = default) =>
|
||||
Set.AnyAsync(u => u.Username == username, ct);
|
||||
|
||||
public Task<bool> ExistsByRoleIdAsync(Guid roleId, CancellationToken ct = default) =>
|
||||
Db.UserRoles.AnyAsync(ur => ur.RoleId == roleId, ct);
|
||||
|
||||
public Task<List<Guid>> GetRoleIdsAsync(Guid userId, CancellationToken ct = default) =>
|
||||
Db.UserRoles.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).ToListAsync(ct);
|
||||
|
||||
public Task<List<User>> SearchWithRoleAsync(string? term, int? take, CancellationToken ct = default)
|
||||
{
|
||||
var query = Set.Include(u => u.UserRoles).ThenInclude(ur => ur.Role).AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
var lower = term.Trim().ToLower();
|
||||
query = query.Where(u => u.Username.ToLower().Contains(lower) || u.DisplayName.ToLower().Contains(lower));
|
||||
}
|
||||
|
||||
query = query.OrderBy(u => u.DisplayName);
|
||||
if (take is { } n)
|
||||
{
|
||||
query = query.Take(n);
|
||||
}
|
||||
|
||||
return query.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public Task<PagedResult<User>> SearchWithRolePagedAsync(string? term, int page, int pageSize, CancellationToken ct = default)
|
||||
{
|
||||
var query = Set.Include(u => u.UserRoles).ThenInclude(ur => ur.Role).AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
var lower = term.Trim().ToLower();
|
||||
query = query.Where(u => u.Username.ToLower().Contains(lower) || u.DisplayName.ToLower().Contains(lower));
|
||||
}
|
||||
|
||||
return query.OrderBy(u => u.DisplayName).ToPagedResultAsync(page, pageSize, ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Common.Repositories;
|
||||
using mws.backend.dotnet.infrastructure.Persistence.Repositories;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.Persistence;
|
||||
|
||||
public class UnitOfWork : IUnitOfWork
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public UnitOfWork(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
Users = new UserRepository(db);
|
||||
Projects = new ProjectRepository(db);
|
||||
Tasks = new TaskRepository(db);
|
||||
Documents = new DocumentRepository(db);
|
||||
Roles = new RoleRepository(db);
|
||||
MasterData = new MasterDataRepository(db);
|
||||
}
|
||||
|
||||
public IUserRepository Users { get; }
|
||||
public IProjectRepository Projects { get; }
|
||||
public ITaskRepository Tasks { get; }
|
||||
public IDocumentRepository Documents { get; }
|
||||
public IRoleRepository Roles { get; }
|
||||
public IMasterDataRepository MasterData { get; }
|
||||
|
||||
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _db.SaveChangesAsync(ct);
|
||||
}
|
||||
Reference in New Issue
Block a user