Skip to content

Commit 67bd24d

Browse files
Add project management support
- Implemented `Project` entity with related domain logic, including visibility, tags, members, and ownership. - Added `ProjectVisibility` enum to define project visibility states. - Created `ProjectService` and `ProjectRepository` for managing project-related operations. - Introduced DTOs (`ProjectDto`, `MemberDto`) for API use cases. - Updated `AppDbContext` to support projects and relationships like members. - Added `ProjectsController` (implementation to follow).
1 parent 8f90b59 commit 67bd24d

File tree

13 files changed

+531
-2
lines changed

13 files changed

+531
-2
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.AspNetCore.Mvc;
3+
using sparkly_server.Services.Projects;
4+
using sparkly_server.Services.Users;
5+
6+
namespace sparkly_server.Controllers.Projects
7+
{
8+
[ApiController]
9+
[Authorize]
10+
public class ProjectsController : ControllerBase
11+
{
12+
private readonly IProjectService _projects;
13+
private readonly ICurrentUser _currentUser;
14+
private readonly IUserService _users;
15+
16+
public ProjectsController(IProjectService projects, ICurrentUser currentUser, IUserService users)
17+
{
18+
_projects = projects;
19+
_currentUser = currentUser;
20+
_users = users;
21+
}
22+
23+
// Controllers soon :)
24+
}
25+
}

src/DTO/Projects/MemberDto.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace sparkly_server.DTO.Projects
2+
{
3+
public class MemberDto
4+
{
5+
public Guid Id { get; set; }
6+
public string UserName { get; set; } = default!;
7+
}
8+
}

src/DTO/Projects/ProjectDto.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace sparkly_server.DTO.Projects
2+
{
3+
public class ProjectDto
4+
{
5+
public Guid Id { get; set; }
6+
public string ProjectName { get; set; } = default!;
7+
public List<MemberDto> Members { get; set; } = new();
8+
}
9+
}

src/Domain/Projects/Project.cs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
using sparkly_server.Enum;
2+
3+
namespace sparkly_server.Domain.Projects
4+
{
5+
public class Project
6+
{
7+
public Guid Id { get; private set; }
8+
public string ProjectName { get; private set; }
9+
public string Description { get; private set; }
10+
public string Slug { get; private set; }
11+
public DateTime CreatedAt { get; private set; }
12+
public Guid OwnerId { get; private set; }
13+
private readonly List<string> _tags = new();
14+
public IReadOnlyCollection<string> Tags => _tags.AsReadOnly();
15+
public DateTime UpdatedAt { get; private set; }
16+
public ProjectVisibility Visibility { get; private set; }
17+
private readonly List<User> _members = new();
18+
public IReadOnlyCollection<User> Members => _members.AsReadOnly();
19+
20+
private Project() { }
21+
22+
public Project(
23+
Guid ownerId,
24+
string projectName,
25+
string description,
26+
ProjectVisibility visibility = ProjectVisibility.Private)
27+
{
28+
Id = Guid.NewGuid();
29+
OwnerId = ownerId;
30+
CreatedAt = DateTime.UtcNow;
31+
UpdatedAt = CreatedAt;
32+
33+
SetNameInternal(projectName);
34+
SetDescriptionInternal(description);
35+
SetVisibilityInternal(visibility);
36+
37+
Slug = NormalizeSlug(projectName);
38+
}
39+
40+
public static Project Create(
41+
User owner,
42+
string projectName,
43+
string description,
44+
ProjectVisibility visibility = ProjectVisibility.Private)
45+
{
46+
if (owner is null)
47+
throw new ArgumentNullException(nameof(owner));
48+
49+
var project = new Project(owner.Id, projectName, description, visibility);
50+
51+
project.AddMember(owner); // owner always member!!!
52+
53+
return project;
54+
}
55+
56+
private void Touch()
57+
{
58+
UpdatedAt = DateTime.UtcNow;
59+
}
60+
61+
/// INTERNAL SETTERS (without Touch) ///
62+
63+
private void SetNameInternal(string name)
64+
{
65+
if (string.IsNullOrWhiteSpace(name))
66+
throw new ArgumentException("Project name cannot be empty.", nameof(name));
67+
68+
ProjectName = name.Trim();
69+
}
70+
71+
private void SetDescriptionInternal(string description)
72+
{
73+
Description = description?.Trim() ?? string.Empty;
74+
}
75+
76+
private void SetVisibilityInternal(ProjectVisibility visibility)
77+
{
78+
Visibility = visibility;
79+
}
80+
81+
private static string NormalizeSlug(string value)
82+
{
83+
return value
84+
.Trim()
85+
.ToLowerInvariant()
86+
.Replace(" ", "-");
87+
}
88+
89+
90+
/// PUBLIC SETTERS (with Touch) ///
91+
92+
public void Rename(string newName)
93+
{
94+
SetNameInternal(newName);
95+
// Jeśli chcesz, aby slug szedł za nazwą:
96+
Slug = NormalizeSlug(newName);
97+
Touch();
98+
}
99+
100+
public void ChangeDescription(string newDescription)
101+
{
102+
SetDescriptionInternal(newDescription);
103+
Touch();
104+
}
105+
106+
public void SetSlug(string slug)
107+
{
108+
if (string.IsNullOrWhiteSpace(slug))
109+
throw new ArgumentException("Slug cannot be empty.", nameof(slug));
110+
111+
Slug = NormalizeSlug(slug);
112+
Touch();
113+
}
114+
115+
public bool HasMember(Guid userId) =>
116+
_members.Any(m => m.Id == userId);
117+
118+
public bool IsOwner(Guid userId) => OwnerId == userId;
119+
120+
public void SetVisibility(ProjectVisibility visibility)
121+
{
122+
SetVisibilityInternal(visibility);
123+
Touch();
124+
}
125+
126+
public void MakePublic()
127+
{
128+
SetVisibilityInternal(ProjectVisibility.Public);
129+
Touch();
130+
}
131+
132+
public void MakePrivate()
133+
{
134+
SetVisibilityInternal(ProjectVisibility.Private);
135+
Touch();
136+
}
137+
138+
public void AddMember(User user)
139+
{
140+
if (user is null)
141+
throw new ArgumentNullException(nameof(user));
142+
143+
if (_members.Any(m => m.Id == user.Id))
144+
return;
145+
146+
_members.Add(user);
147+
Touch();
148+
}
149+
150+
public void RemoveMember(User user)
151+
{
152+
if (user is null)
153+
throw new ArgumentNullException(nameof(user));
154+
155+
if (_members.Any(m => m.Id == user.Id))
156+
return;
157+
158+
_members.Remove(user);
159+
Touch();
160+
}
161+
162+
public void AddTag(string tag)
163+
{
164+
if (string.IsNullOrWhiteSpace(tag))
165+
return;
166+
167+
tag = tag.Trim();
168+
169+
if (_tags.Contains(tag, StringComparer.OrdinalIgnoreCase))
170+
return;
171+
172+
_tags.Add(tag);
173+
Touch();
174+
}
175+
176+
public void RemoveTag(string tag)
177+
{
178+
if (string.IsNullOrWhiteSpace(tag))
179+
return;
180+
181+
var existing = _tags
182+
.FirstOrDefault(t => string.Equals(t, tag, StringComparison.OrdinalIgnoreCase));
183+
184+
if (existing is null)
185+
return;
186+
187+
_tags.Remove(existing);
188+
Touch();
189+
}
190+
}
191+
}

src/Enum/ProjectVisibility.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace sparkly_server.Enum
2+
{
3+
public enum ProjectVisibility
4+
{
5+
Private = 0,
6+
Public = 1
7+
}
8+
}

src/Infrastructure/AppDbContext.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
using Microsoft.EntityFrameworkCore;
22
using sparkly_server.Domain;
33
using sparkly_server.Domain.Auth;
4+
using sparkly_server.Domain.Projects;
45

56
namespace sparkly_server.Infrastructure
67
{
78
public class AppDbContext : DbContext
89
{
910
public DbSet<User> Users => Set<User>();
11+
public DbSet<Project> Projects => Set<Project>();
1012
public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;
1113

1214
public AppDbContext(DbContextOptions<AppDbContext> options)
@@ -61,6 +63,45 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
6163
cfg.Property(rt => rt.ExpiresAt)
6264
.IsRequired();
6365
});
66+
67+
// Projects
68+
modelBuilder.Entity<Project>(cfg =>
69+
{
70+
cfg.ToTable("projects");
71+
72+
cfg.HasKey(p => p.Id);
73+
74+
cfg.Property(p => p.ProjectName)
75+
.IsRequired()
76+
.HasMaxLength(200);
77+
78+
cfg.Property(p => p.Description)
79+
.HasMaxLength(2000);
80+
81+
cfg.Property(p => p.Slug)
82+
.IsRequired()
83+
.HasMaxLength(256);
84+
85+
cfg.Property(p => p.CreatedAt)
86+
.IsRequired();
87+
88+
cfg.Property(p => p.UpdatedAt)
89+
.IsRequired();
90+
91+
cfg.Property(p => p.OwnerId)
92+
.IsRequired();
93+
94+
cfg.Property(p => p.Visibility)
95+
.IsRequired();
96+
97+
cfg.HasMany(typeof(User), "_members")
98+
.WithMany("_projects")
99+
.UsingEntity(j =>
100+
{
101+
j.ToTable("project_members");
102+
});
103+
}
104+
);
64105
}
65106
}
66107
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using sparkly_server.Domain.Projects;
2+
3+
namespace sparkly_server.Services.Projects
4+
{
5+
public interface IProjectRepository
6+
{
7+
Task AddAsync(Project project, CancellationToken cancellationToken = default);
8+
Task<Project?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
9+
Task<IReadOnlyList<Project>> GetForUserAsync(Guid userId, CancellationToken cancellationToken = default);
10+
11+
Task SaveChangesAsync(CancellationToken cancellationToken = default);
12+
}
13+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using sparkly_server.Domain.Projects;
2+
using sparkly_server.Enum;
3+
4+
namespace sparkly_server.Services.Projects
5+
{
6+
public interface IProjectService
7+
{
8+
Task<Project> CreateProjectAsync(
9+
string name,
10+
string description,
11+
ProjectVisibility visibility,
12+
CancellationToken cancellationToken = default);
13+
14+
Task<Project> GetProjectByIdAsync(
15+
Guid projectId, CancellationToken
16+
cancellationToken = default);
17+
18+
Task<IReadOnlyList<Project>> GetProjectsForCurrentUserAsync(
19+
CancellationToken cancellationToken = default);
20+
21+
Task RenameAsync(
22+
Guid projectId,
23+
string newName,
24+
CancellationToken cancellationToken = default);
25+
26+
Task ChangeDescriptionAsync(
27+
Guid projectId,
28+
string newDescription,
29+
CancellationToken cancellationToken = default);
30+
31+
Task SetVisibilityAsync(
32+
Guid projectId,
33+
ProjectVisibility visibility,
34+
CancellationToken cancellationToken = default);
35+
36+
Task AddMemberAsync(
37+
Guid projectId,
38+
Guid userId,
39+
CancellationToken cancellationToken = default);
40+
41+
Task RemoveMemberAsync(
42+
Guid projectId,
43+
Guid userId,
44+
CancellationToken cancellationToken = default);
45+
}
46+
}

0 commit comments

Comments
 (0)