Skip to content

Commit 9464f9a

Browse files
Add project feed functionality
- Implemented `GetFeedAsync` in `ProjectService` to retrieve paginated project feeds based on query parameters. - Added `GetQueryFeedAsync` in `ProjectRepository` for database-level feed handling. - Introduced new DTOs (`ProjectFeedQuery`, `ProjectFeedItem`, `ProjectFeedData`, and `ProjectFeedResult`) to support feed queries and responses. - Extended `ProjectsController` with a new `/feed` endpoint for project feed retrieval. - Updated XML comments for all related methods and entities to improve documentation.
1 parent 44756b3 commit 9464f9a

File tree

10 files changed

+217
-14
lines changed

10 files changed

+217
-14
lines changed
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbFunctionsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fc1c46ed28c61e1caa79185e4375a8ae7cd11cd5ba8853dcb37577f93f2ca8d5_003FDbFunctionsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
23
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APrincipalExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fcc1ad596b4fde937674ff6c832655e53b7c6cc97b1b7a38893ad352a788057_003FPrincipalExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
3-
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=64599883_002D44d5_002D4d21_002D921f_002D1ab4ff07b455/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="CreateProject_Should_Create_Project_For_Authenticated_User" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
4-
&lt;TestAncestor&gt;
5-
&lt;TestId&gt;xUnit::E26AB9F3-8A34-4AB8-A503-F0B851823527::net9.0::sparkly_server.test.ProjectTest.CreateProject_Should_Create_Project_For_Authenticated_User&lt;/TestId&gt;
6-
&lt;/TestAncestor&gt;
4+
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=64599883_002D44d5_002D4d21_002D921f_002D1ab4ff07b455/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="CreateProject_Should_Create_Project_For_Authenticated_User" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
5+
&lt;TestAncestor&gt;&#xD;
6+
&lt;TestId&gt;xUnit::E26AB9F3-8A34-4AB8-A503-F0B851823527::net9.0::sparkly_server.test.ProjectTest.CreateProject_Should_Create_Project_For_Authenticated_User&lt;/TestId&gt;&#xD;
7+
&lt;/TestAncestor&gt;&#xD;
78
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>

src/Controllers/Projects/ProjectsController.cs

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.AspNetCore.Authorization;
22
using Microsoft.AspNetCore.Mvc;
33
using sparkly_server.DTO.Projects;
4+
using sparkly_server.DTO.Projects.Feed;
45
using sparkly_server.Services.Projects;
56

67
namespace sparkly_server.Controllers.Projects
@@ -13,24 +14,54 @@ public class ProjectsController : ControllerBase
1314
private readonly IProjectService _projects;
1415

1516
public ProjectsController(IProjectService projects) => _projects = projects;
16-
17-
// Random projects to feed the homepage
17+
18+
/// <summary>
19+
/// Retrieves a specified number of random public projects.
20+
/// </summary>
21+
/// <param name="take">The number of projects to retrieve. Defaults to 20 if not specified.</param>
22+
/// <param name="ct">A token to observe while waiting for the task to complete.</param>
23+
/// <returns>An HTTP response containing a list of random public projects.</returns>
1824
[HttpGet("random")]
1925
public async Task<IActionResult> GetRandomProjects([FromQuery] int take = 20, CancellationToken ct = default)
2026
{
2127
var response = await _projects.GetRandomPublicAsync(take, ct);
2228
return Ok(response);
2329
}
24-
25-
// Get project by id
30+
31+
/// <summary>
32+
/// Retrieves a feed of projects based on the specified query parameters.
33+
/// </summary>
34+
/// <param name="query">The query parameters used to filter and paginate the project feed.</param>
35+
/// <param name="ct">A token to observe while waiting for the task to complete.</param>
36+
/// <returns>An HTTP response containing the requested project feed.</returns>
37+
[HttpGet("feed")]
38+
public async Task<IActionResult> GetProjectsFeed(
39+
[FromQuery] ProjectFeedQuery query,
40+
CancellationToken ct = default)
41+
{
42+
var feed = await _projects.GetFeedAsync(query, ct);
43+
return Ok(feed);
44+
}
45+
46+
47+
/// <summary>
48+
/// Retrieves a project by its unique identifier.
49+
/// </summary>
50+
/// <param name="projectId">The unique identifier of the project to retrieve.</param>
51+
/// <param name="ct">A token to observe while waiting for the task to complete.</param>
52+
/// <returns>An HTTP response containing the project details.</returns>
2653
[HttpGet("{projectId:guid}")]
2754
public async Task<IActionResult> GetProjectById(Guid projectId, CancellationToken ct = default)
2855
{
2956
var project = await _projects.GetProjectByIdAsync(projectId, ct);
3057
return Ok(project);
3158
}
32-
33-
// Create project
59+
60+
/// <summary>
61+
/// Creates a new project with the specified details.
62+
/// </summary>
63+
/// <param name="request">The request object containing the project name, description, and visibility settings.</param>
64+
/// <returns>A response containing the created project's details.</returns>
3465
[HttpPost("create")]
3566
public async Task<IActionResult> CreateProject([FromBody] CreateProjectRequest request)
3667
{
@@ -41,7 +72,13 @@ public async Task<IActionResult> CreateProject([FromBody] CreateProjectRequest r
4172
return Ok(response);
4273
}
4374

44-
// Update project by id (admin only)
75+
/// <summary>
76+
/// Updates the details of an existing project. (Admin project only)
77+
/// </summary>
78+
/// <param name="projectId">The unique identifier of the project to be updated.</param>
79+
/// <param name="request">An object containing the updated project details, such as name, description, and visibility.</param>
80+
/// <param name="cn">A token to observe while waiting for the task to complete.</param>
81+
/// <returns>A no-content HTTP response indicating the project was successfully updated.</returns>
4582
[HttpPut("update/{projectId:guid}")]
4683
public async Task<IActionResult> UpdateProject(
4784
Guid projectId,
@@ -51,8 +88,13 @@ public async Task<IActionResult> UpdateProject(
5188
await _projects.UpdateProjectAsync(projectId, request, cn);
5289
return NoContent();
5390
}
54-
55-
// Delete project by id (admin only)
91+
92+
/// <summary>
93+
/// Deletes a project identified by its unique ID. (Admin project only)
94+
/// </summary>
95+
/// <param name="projectId">The unique identifier of the project to delete.</param>
96+
/// <param name="ct">A token to observe while waiting for the task to complete.</param>
97+
/// <returns>An HTTP response with no content if the deletion was successful.</returns>
5698
[HttpDelete("delete/{projectId:guid}")]
5799
public async Task<IActionResult> DeleteProject(Guid projectId, CancellationToken ct = default)
58100
{
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using sparkly_server.Domain.Projects;
2+
3+
namespace sparkly_server.DTO.Projects.Feed
4+
{
5+
public sealed class ProjectFeedData
6+
{
7+
public required IReadOnlyList<Project> Items { get; init; }
8+
public required int TotalCount { get; init; }
9+
public required int Page { get; init; }
10+
public required int PageSize { get; init; }
11+
}
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using sparkly_server.Enum;
2+
3+
namespace sparkly_server.DTO.Projects.Feed
4+
{
5+
public class ProjectFeedItem
6+
{
7+
public Guid Id { get; init; }
8+
public required string ProjectName { get; init; }
9+
public string? Description { get; init; }
10+
public required string OwnerUserName { get; init; }
11+
public DateTime CreatedAt { get; init; }
12+
public ProjectVisibility Visibility { get; init; }
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using sparkly_server.Enum;
2+
3+
namespace sparkly_server.DTO.Projects.Feed
4+
{
5+
public sealed class ProjectFeedQuery
6+
{
7+
public Guid? OwnerId { get; set; }
8+
public bool Mine { get; set; }
9+
public ProjectVisibility? Visibility { get; set; }
10+
public string? Search { get; set; }
11+
public int Page { get; set; } = 1;
12+
public int PageSize { get; set; } = 20;
13+
}
14+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace sparkly_server.DTO.Projects.Feed
2+
{
3+
public sealed class ProjectFeedResult
4+
{
5+
public required IReadOnlyList<ProjectFeedItem> Items { get; init; }
6+
public required int Page { get; init; }
7+
public required int PageSize { get; init; }
8+
public required int TotalCount { get; init; }
9+
}
10+
}

src/Services/Projects/IProjectRepository.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using sparkly_server.Domain.Projects;
2+
using sparkly_server.DTO.Projects;
3+
using sparkly_server.DTO.Projects.Feed;
24

35
namespace sparkly_server.Services.Projects
46
{
@@ -11,5 +13,6 @@ public interface IProjectRepository
1113
Task<IReadOnlyList<Project>> GetRandomPublicAsync(int take, CancellationToken ct = default);
1214
Task SaveChangesAsync(CancellationToken cancellationToken = default);
1315
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
16+
Task<ProjectFeedData> GetQueryFeedAsync(ProjectFeedQuery query, Guid? currentUserId, CancellationToken cancellationToken = default);
1417
}
1518
}

src/Services/Projects/IProjectService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using sparkly_server.Domain.Projects;
22
using sparkly_server.DTO.Projects;
3+
using sparkly_server.DTO.Projects.Feed;
34
using sparkly_server.Enum;
45

56
namespace sparkly_server.Services.Projects
@@ -56,5 +57,9 @@ Task UpdateProjectAsync(
5657
Task DeleteProjectAsync(
5758
Guid projectId,
5859
CancellationToken cancellationToken = default);
60+
61+
Task<ProjectFeedResult> GetFeedAsync(
62+
ProjectFeedQuery query,
63+
CancellationToken cancellationToken = default);
5964
}
6065
}

src/Services/Projects/ProjectRepository.cs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using Microsoft.EntityFrameworkCore;
22
using sparkly_server.Domain.Projects;
3+
using sparkly_server.DTO.Projects;
4+
using sparkly_server.DTO.Projects.Feed;
35
using sparkly_server.Enum;
46
using sparkly_server.Infrastructure;
57

@@ -35,7 +37,7 @@ public async Task AddAsync(Project project, CancellationToken cancellationToken
3537
public Task<Project?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
3638
{
3739
return _db.Projects
38-
.Include(p => p.Members) // jak masz relacje
40+
.Include(p => p.Members)
3941
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
4042
}
4143

@@ -87,6 +89,64 @@ await _db.Projects
8789
.ExecuteDeleteAsync(cancellationToken);
8890
}
8991

92+
/// <summary>
93+
/// Retrieves a paginated feed of projects based on the specified query criteria and the current user's context asynchronously.
94+
/// </summary>
95+
/// <param name="query">The query object containing the filtering and pagination details.</param>
96+
/// <param name="currentUserId">The unique identifier of the current user, if available, for personalized results.</param>
97+
/// <param name="cancellationToken">A CancellationToken to observe while waiting for the task to complete.</param>
98+
/// <returns>A Task representing the asynchronous operation, containing a ProjectFeedData instance with the query results.</returns>
99+
public async Task<ProjectFeedData> GetQueryFeedAsync(ProjectFeedQuery query, Guid? currentUserId,
100+
CancellationToken cancellationToken = default)
101+
{
102+
var projects = _db.Projects.AsQueryable();
103+
104+
// Mine projects
105+
if (query.Mine && currentUserId is not null)
106+
{
107+
projects = projects.Where(p => p.OwnerId == currentUserId);
108+
}
109+
110+
// filter by ownerId
111+
if (query.OwnerId is not null)
112+
{
113+
projects = projects.Where(p => p.OwnerId == query.OwnerId);
114+
}
115+
116+
// filter by visibility
117+
if (query.Visibility is not null)
118+
{
119+
projects = projects.Where(p => p.Visibility == query.Visibility);
120+
}
121+
122+
// filter by search
123+
if (!string.IsNullOrWhiteSpace(query.Search))
124+
{
125+
var s = query.Search.Trim();
126+
projects = projects.Where(p => EF.Functions.Like(p.ProjectName, $"%{s}%"));
127+
}
128+
129+
var totalCount = await projects.CountAsync(cancellationToken);
130+
131+
var page = Math.Max(query.Page, 1);
132+
var pageSize = Math.Min(query.PageSize, 100);
133+
134+
var items = await projects
135+
.OrderByDescending(p => p.CreatedAt)
136+
.Skip((page - 1) * pageSize)
137+
.Take(pageSize)
138+
.Include(p => p.Members)
139+
.ToListAsync(cancellationToken);
140+
141+
return new ProjectFeedData
142+
{
143+
Items = items,
144+
TotalCount = totalCount,
145+
Page = page,
146+
PageSize = pageSize
147+
};
148+
}
149+
90150
/// <summary>
91151
/// Retrieves a random list of public projects asynchronously.
92152
/// </summary>

src/Services/Projects/ProjectService.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using sparkly_server.Domain.Projects;
22
using sparkly_server.DTO.Projects;
3+
using sparkly_server.DTO.Projects.Feed;
34
using sparkly_server.Enum;
45
using sparkly_server.Services.Users;
56

@@ -353,5 +354,46 @@ public async Task DeleteProjectAsync(Guid projectId, CancellationToken cancellat
353354

354355
await _projects.SaveChangesAsync(cancellationToken);
355356
}
357+
358+
/// <summary>
359+
/// Retrieves a feed of projects based on the specified query and current user's context.
360+
/// </summary>
361+
/// <param name="query">The query parameters used to filter and paginate the project feed results.</param>
362+
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
363+
/// <returns>Returns a result containing a list of projects matching the query, as well as pagination details.</returns>
364+
/// <exception cref="InvalidOperationException">
365+
/// Thrown if the current user's context cannot be resolved.
366+
/// </exception>
367+
public async Task<ProjectFeedResult> GetFeedAsync(ProjectFeedQuery query, CancellationToken cancellationToken = default)
368+
{
369+
var currentUserId = _currentUser.UserId;
370+
371+
var data = await _projects.GetQueryFeedAsync(query, currentUserId, cancellationToken);
372+
373+
var items = data.Items.Select(p =>
374+
{
375+
var owner = p.Members.FirstOrDefault(m => m.Id == p.OwnerId);
376+
377+
return new ProjectFeedItem
378+
{
379+
Id = p.Id,
380+
ProjectName = p.ProjectName,
381+
Description = p.Description,
382+
OwnerUserName = owner?.UserName ?? "Unknown",
383+
CreatedAt = p.CreatedAt,
384+
Visibility = p.Visibility
385+
};
386+
}).ToList();
387+
388+
return new ProjectFeedResult
389+
{
390+
Items = items,
391+
Page = data.Page,
392+
PageSize = data.PageSize,
393+
TotalCount = data.TotalCount
394+
};
395+
}
396+
397+
356398
}
357399
}

0 commit comments

Comments
 (0)