Skip to content

Commit d7c6902

Browse files
authored
Merge pull request #27 from FakeOverFlow/shreyas/list-answers-post-answers
shreyas/list-answers-post-answers - Added endpoints for answers on posts
2 parents d076dbf + 628853b commit d7c6902

9 files changed

Lines changed: 306 additions & 7 deletions

File tree

apps/fakeoverflow-api/FakeOverFlow/FakeoverFlow.Backend.Http.Api/Abstracts/Services/IPostService.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using FakeoverFlow.Backend.Abstraction;
2+
using FakeoverFlow.Backend.Http.Api.Features.Posts.Contents.CreateContent;
23
using FakeoverFlow.Backend.Http.Api.Models.Posts;
34
using Posts = FakeoverFlow.Backend.Http.Api.Features.Posts.CreatePosts.Posts;
45

@@ -67,6 +68,7 @@ public interface IPostService
6768

6869
Task<Result<(IEnumerable<(Models.Posts.Posts post, PostContent? content)> items, long totalCount)>>
6970
ListPostsAsync(int page, int pageSize, IEnumerable<string> tags, CancellationToken ct = default);
71+
7072
/// <summary>
7173
/// Retrieves a paginated list of tags along with their respective post counts based on the specified query parameters.
7274
/// </summary>
@@ -80,4 +82,30 @@ public interface IPostService
8082
/// </returns>
8183
public Task<(List<Tag> Tags, Dictionary<int, int> TagCounts)> GetTagsWithCountsAsync(int page = 0,
8284
int pageSize = 10, string? searchTerm = null, CancellationToken ct = default);
85+
86+
/// <summary>
87+
/// Creates a new content entry associated with a specific post based on the provided request data.
88+
/// </summary>
89+
/// <param name="request">
90+
/// The data needed to create the content, including the content text and associated post ID.
91+
/// </param>
92+
/// <param name="ct">A cancellation token to manage task cancellation, if necessary.</param>
93+
/// <returns>
94+
/// A task representing the asynchronous operation. The task result contains the created post content.
95+
/// </returns>
96+
public Task<PostContent> CreatePostContentAsync(CreateContent.Request request, CancellationToken ct = default);
97+
98+
/// <summary>
99+
/// Retrieves the list of post contents associated with a specific post ID that are marked as answers.
100+
/// </summary>
101+
/// <param name="postId">
102+
/// The unique identifier of the post whose contents are to be retrieved.
103+
/// </param>
104+
/// <param name="ct">
105+
/// A cancellation token used to manage task cancellation.
106+
/// </param>
107+
/// <returns>
108+
/// A task representing the asynchronous operation. The task result contains a list of post contents associated with the specified post ID.
109+
/// </returns>
110+
public Task<List<PostContent>> GetPostContentByPostIdAsync(string postId, CancellationToken ct = default);
83111
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using FastEndpoints;
2+
3+
namespace FakeoverFlow.Backend.Http.Api.Features.Posts.Contents;
4+
5+
public sealed class ContentsGroup : Group
6+
{
7+
public ContentsGroup()
8+
{
9+
Configure("/{postId}/contents", x =>
10+
{
11+
x.Group<PostGroup>();
12+
x.Description(b =>
13+
{
14+
b.WithGroupName("Post Content");
15+
b.WithSummary("Post Content of a post related endpoints");
16+
b.WithDescription("Endpoints for post content related operations");
17+
});
18+
x.EndpointVersion(0);
19+
});
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NSwag.Annotations;
2+
3+
namespace FakeoverFlow.Backend.Http.Api.Features.Posts.Contents.CreateContent;
4+
5+
public partial class CreateContent
6+
{
7+
public class Request
8+
{
9+
public string Content { get; set; }
10+
11+
[OpenApiIgnore] public string PostId { get; set; } = string.Empty;
12+
13+
public bool IsInternal { get; set; }
14+
}
15+
16+
public class Response
17+
{
18+
public required string PostId { get; set; }
19+
20+
public Guid Id { get; set; }
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using FakeoverFlow.Backend.Http.Api.Abstracts.Services;
2+
using FastEndpoints;
3+
using Microsoft.AspNetCore.Http.HttpResults;
4+
5+
namespace FakeoverFlow.Backend.Http.Api.Features.Posts.Contents.CreateContent;
6+
7+
public partial class CreateContent
8+
{
9+
public class Handler(
10+
IPostService postService,
11+
ILogger<CreateContent> logger
12+
) : Endpoint<Request, Results<Ok<Response>, BadRequest<ErrorResponse>, ProblemHttpResult>>
13+
{
14+
public override void Configure()
15+
{
16+
Post("");
17+
Group<ContentsGroup>();
18+
Description(x => x.WithName("CreateAnswer"));
19+
}
20+
21+
public override async Task<Results<Ok<Response>, BadRequest<ErrorResponse>, ProblemHttpResult>> ExecuteAsync(
22+
Request req, CancellationToken ct)
23+
{
24+
logger.LogInformation("Creating answer");
25+
if (!HttpContext.Request.RouteValues.TryGetValue("postId", out object? postIdRaw))
26+
{
27+
AddError(">" + (postIdRaw?.ToString() ?? "N/A") + "< is not a valid postId");
28+
ThrowIfAnyErrors();
29+
}
30+
31+
var postId = postIdRaw!.ToString();
32+
if (string.IsNullOrWhiteSpace(postId))
33+
{
34+
AddError(">" + (postIdRaw?.ToString() ?? "N/A") + "< is not a valid postId");
35+
ThrowIfAnyErrors();
36+
}
37+
38+
req.PostId = postId!.ToUpper();
39+
40+
var content = await postService.CreatePostContentAsync(req, ct);
41+
42+
return TypedResults.Ok(new Response()
43+
{
44+
PostId = content.PostId,
45+
Id = content.Id
46+
});
47+
}
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using FastEndpoints;
2+
using FluentValidation;
3+
4+
namespace FakeoverFlow.Backend.Http.Api.Features.Posts.Contents.CreateContent;
5+
6+
public partial class CreateContent
7+
{
8+
public class Validator : Validator<Request>
9+
{
10+
public Validator()
11+
{
12+
RuleFor(x => x.Content)
13+
.NotEmpty();
14+
15+
RuleFor(x => x.Content)
16+
.MinimumLength(4);
17+
}
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace FakeoverFlow.Backend.Http.Api.Features.Posts.Contents.ListContents;
2+
3+
public partial class ListContents
4+
{
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace FakeoverFlow.Backend.Http.Api.Features.Posts.Contents.ListContents;
2+
3+
public partial class ListContents
4+
{
5+
public class Request
6+
{
7+
public string PostId { get; set; } = string.Empty;
8+
}
9+
10+
public class Response
11+
{
12+
public List<PostContent> Answers { get; set; } = [];
13+
}
14+
15+
public class PostContent
16+
{
17+
public Guid Id { get; set; }
18+
19+
public string PostId { get; set; } = string.Empty;
20+
public string Content { get; set; } = string.Empty;
21+
22+
public bool IsInternal { get; set; }
23+
24+
public required UserActivity CreatedOn { get; set; }
25+
}
26+
27+
public class UserActivity
28+
{
29+
public Guid UserId { get; set; }
30+
31+
public DateTimeOffset ActivityOn { get; set; }
32+
public required string Username { get; set; }
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using FakeoverFlow.Backend.Http.Api.Abstracts.Services;
2+
using FastEndpoints;
3+
using Microsoft.AspNetCore.Http.HttpResults;
4+
5+
namespace FakeoverFlow.Backend.Http.Api.Features.Posts.Contents.ListContents;
6+
7+
public partial class ListContents
8+
{
9+
public class Handler(
10+
IPostService postService,
11+
ILogger<ListContents> logger
12+
) : Endpoint<Request, Results<Ok<Response>, BadRequest<ErrorResponse>, ProblemHttpResult>>
13+
{
14+
public override async Task<Results<Ok<Response>, BadRequest<ErrorResponse>, ProblemHttpResult>> ExecuteAsync(
15+
Request req, CancellationToken ct)
16+
{
17+
if (!HttpContext.Request.RouteValues.TryGetValue("postId", out object? postIdRaw))
18+
{
19+
AddError(">" + (postIdRaw?.ToString() ?? "N/A") + "< is not a valid postId");
20+
ThrowIfAnyErrors();
21+
}
22+
23+
var postId = postIdRaw!.ToString();
24+
if (string.IsNullOrWhiteSpace(postId))
25+
{
26+
AddError(">" + (postIdRaw?.ToString() ?? "N/A") + "< is not a valid postId");
27+
ThrowIfAnyErrors();
28+
}
29+
30+
req.PostId = postId!;
31+
var postContents = await postService.GetPostContentByPostIdAsync(req.PostId, ct);
32+
return TypedResults.Ok(new Response()
33+
{
34+
Answers = postContents.Select(x => new PostContent()
35+
{
36+
Content = x.Content,
37+
Id = x.Id,
38+
PostId = x.PostId,
39+
CreatedOn = new UserActivity()
40+
{
41+
ActivityOn = x.CreatedOn,
42+
UserId = x.CreatedByAccount.Id,
43+
Username = x.CreatedByAccount.Username
44+
}
45+
}).ToList()
46+
});
47+
}
48+
49+
public override void Configure()
50+
{
51+
Get("");
52+
AllowAnonymous();
53+
Description(x => { x.WithName("ListAnswers").WithSummary("Lists all answers"); });
54+
Group<ContentsGroup>();
55+
}
56+
}
57+
}

apps/fakeoverflow-api/FakeOverFlow/FakeoverFlow.Backend.Http.Api/Services/PostService.cs

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using FakeoverFlow.Backend.Abstraction;
22
using FakeoverFlow.Backend.Abstraction.Context;
33
using FakeoverFlow.Backend.Http.Api.Abstracts.Services;
4+
using FakeoverFlow.Backend.Http.Api.Features.Posts.Contents.CreateContent;
45
using FakeoverFlow.Backend.Http.Api.Models.Enums;
56
using FakeoverFlow.Backend.Http.Api.Models.Posts;
67
using FakeoverFlow.Backend.Http.Api.Utils;
@@ -143,13 +144,14 @@ public async Task<bool> IncreaseViewCountAsync(string id, CancellationToken ct =
143144
.OrderByDescending(p => p.CreatedOn)
144145
.AsQueryable();
145146

146-
/*
147147
if (normalizedTags.Any())
148148
{
149+
var tsQuery = EF.Functions.ToTsQuery(string.Join(" | ", normalizedTags));
150+
149151
postsQuery = postsQuery.Where(p =>
150-
p.Tags.Any(pt => normalizedTags.Contains(pt.Tag.Name.ToLower())));
152+
p.Tags.Any(pt => pt.Tag.VectorText.Matches(tsQuery))
153+
);
151154
}
152-
*/
153155

154156
var totalCount = await postsQuery.LongCountAsync(ct);
155157

@@ -164,15 +166,77 @@ public async Task<bool> IncreaseViewCountAsync(string id, CancellationToken ct =
164166
.Where(x => pageId.Contains(x.PostId) && x.ContentType == ContentType.Questions)
165167
.ToList();
166168

167-
var items = pagedPosts.Select(x =>
168-
{
169-
return (x, contents.FirstOrDefault(c => c.PostId == x.Id));
170-
});
169+
var items = pagedPosts.Select(x => { return (x, contents.FirstOrDefault(c => c.PostId == x.Id)); });
171170

172171
return Result<(IEnumerable<(Posts post, PostContent? content)> items, long totalCount)>
173172
.Success((items, totalCount));
174173
}
175174

175+
public async Task<(List<Tag> Tags, Dictionary<int, int> TagCounts)> GetTagsWithCountsAsync(int page = 0,
176+
int pageSize = 10, string? searchTerm = null, CancellationToken ct = default)
177+
{
178+
var postTagsEnumerable = dbContext.PostTags
179+
.Include(pt => pt.Tag)
180+
.AsQueryable()
181+
.AsNoTracking();
182+
183+
if (!string.IsNullOrWhiteSpace(searchTerm))
184+
{
185+
postTagsEnumerable =
186+
postTagsEnumerable.Where(pt => pt.Tag.VectorText.Matches(EF.Functions.ToTsQuery(searchTerm)));
187+
logger.LogInformation("Searching for {SearchTerm}", searchTerm);
188+
}
189+
190+
191+
var result = postTagsEnumerable
192+
.GroupBy(pt => pt.Tag)
193+
.Select(g => new
194+
{
195+
Tag = g.Key,
196+
Count = g.Count()
197+
})
198+
.Skip(page * pageSize)
199+
.Take(pageSize)
200+
.ToList();
201+
202+
return (result.Select(x => x.Tag).ToList(), result.ToDictionary(x => x.Tag.Id, x => x.Count));
203+
}
204+
205+
public async Task<PostContent> CreatePostContentAsync(CreateContent.Request request, CancellationToken ct = default)
206+
{
207+
request.PostId = request.PostId.ToUpper();
208+
var requestContext = contextFactory.RequestContext;
209+
var now = DateTimeOffset.UtcNow;
210+
var postContent = new PostContent()
211+
{
212+
Id = Guid.CreateVersion7(),
213+
Content = request.Content,
214+
PostId = request.PostId,
215+
ContentType = ContentType.Answers,
216+
CreatedBy = requestContext.UserId,
217+
CreatedOn = now,
218+
UpdatedBy = requestContext.UserId,
219+
UpdatedOn = now,
220+
Votes = 0,
221+
};
222+
223+
dbContext.PostContent.Add(postContent);
224+
await dbContext.SaveChangesAsync(ct);
225+
return postContent;
226+
}
227+
228+
public async Task<List<PostContent>> GetPostContentByPostIdAsync(string postId, CancellationToken ct = default)
229+
{
230+
postId = postId.ToUpper();
231+
return await dbContext.PostContent
232+
.AsNoTracking()
233+
.Where(x => x.PostId == postId && x.ContentType == ContentType.Answers)
234+
.Include(x => x.CreatedByAccount)
235+
.OrderByDescending(x => x.Votes)
236+
.ThenByDescending(x => x.CreatedOn)
237+
.ToListAsync(ct);
238+
}
239+
176240
private async Task<List<Tag>> GetOrCreateTagsAsync(List<string> tags, CancellationToken ct = default)
177241
{
178242
var normalizedTags = tags

0 commit comments

Comments
 (0)