Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pagination #23

Merged
merged 3 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using CrowdParlay.Social.Application.Abstractions;
using MassTransit;

namespace CrowdParlay.Social.Application.Consumers;
namespace CrowdParlay.Social.Api.Consumers;

public class UserEventConsumer : IConsumer<UserCreatedEvent>, IConsumer<UserUpdatedEvent>, IConsumer<UserDeletedEvent>
{
Expand Down
2 changes: 1 addition & 1 deletion src/CrowdParlay.Social.Api/Extensions/ConfigureServices.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using CrowdParlay.Communication;
using CrowdParlay.Social.Api.Consumers;
using CrowdParlay.Social.Api.Middlewares;
using CrowdParlay.Social.Application.Consumers;
using MassTransit;

namespace CrowdParlay.Social.Api.Extensions;
Expand Down
16 changes: 8 additions & 8 deletions src/CrowdParlay.Social.Api/v1/Controllers/CommentsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ public async Task<CommentDto> GetCommentById([FromRoute] Guid commentId) =>
[ProducesResponseType(typeof(IEnumerable<CommentDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(Problem), (int)HttpStatusCode.InternalServerError)]
[ProducesResponseType(typeof(ValidationProblem), (int)HttpStatusCode.BadRequest)]
public async Task<IEnumerable<CommentDto>> SearchComments(
public async Task<Page<CommentDto>> SearchComments(
[FromQuery] Guid? discussionId,
[FromQuery] Guid? authorId,
[FromQuery, BindRequired] int page,
[FromQuery, BindRequired] int size) =>
await _comments.SearchAsync(discussionId, authorId, page, size);
[FromQuery, BindRequired] int offset,
[FromQuery, BindRequired] int count) =>
await _comments.SearchAsync(discussionId, authorId, offset, count);

/// <summary>
/// Creates a top-level comment in discussion.
Expand Down Expand Up @@ -66,11 +66,11 @@ public async Task<ActionResult<CommentDto>> Create([FromBody] CommentRequest req
[ProducesResponseType(typeof(Problem), (int)HttpStatusCode.InternalServerError)]
[ProducesResponseType(typeof(ValidationProblem), (int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(Problem), (int)HttpStatusCode.NotFound)]
public async Task<IEnumerable<CommentDto>> GetRepliesToComment(
public async Task<Page<CommentDto>> GetRepliesToComment(
[FromRoute] Guid parentCommentId,
[FromQuery, BindRequired] int page,
[FromQuery, BindRequired] int size) =>
await _comments.GetRepliesToCommentAsync(parentCommentId, page, size);
[FromQuery, BindRequired] int offset,
[FromQuery, BindRequired] int count) =>
await _comments.GetRepliesToCommentAsync(parentCommentId, offset, count);

/// <summary>
/// Creates a reply to the comment with the specified ID.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ namespace CrowdParlay.Social.Application.Abstractions;
public interface ICommentRepository
{
public Task<CommentDto> GetByIdAsync(Guid id);
public Task<IEnumerable<CommentDto>> SearchAsync(Guid? discussionId, Guid? authorId, int page, int size);
public Task<Page<CommentDto>> SearchAsync(Guid? discussionId, Guid? authorId, int offset, int count);
public Task<CommentDto> CreateAsync(Guid authorId, Guid discussionId, string content);
public Task<IEnumerable<CommentDto>> GetRepliesToCommentAsync(Guid parentCommentId, int page, int size);
public Task<Page<CommentDto>> GetRepliesToCommentAsync(Guid parentCommentId, int offset, int count);
public Task<CommentDto> ReplyToCommentAsync(Guid authorId, Guid parentCommentId, string content);
public Task DeleteAsync(Guid id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="Neo4jClient" Version="5.1.11" />
<PackageReference Include="Serilog" Version="3.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageReference Include="Serilog.Settings.AppSettings" Version="2.2.2" />
Expand Down
2 changes: 1 addition & 1 deletion src/CrowdParlay.Social.Application/DTOs/CommentDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
public class CommentDto
{
public Guid Id { get; set; }
public string Content { get; set; }

Check warning on line 6 in src/CrowdParlay.Social.Application/DTOs/CommentDto.cs

View workflow job for this annotation

GitHub Actions / Build & test

Non-nullable property 'Content' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 6 in src/CrowdParlay.Social.Application/DTOs/CommentDto.cs

View workflow job for this annotation

GitHub Actions / Build & test

Non-nullable property 'Content' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public AuthorDto Author { get; set; }

Check warning on line 7 in src/CrowdParlay.Social.Application/DTOs/CommentDto.cs

View workflow job for this annotation

GitHub Actions / Build & test

Non-nullable property 'Author' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 7 in src/CrowdParlay.Social.Application/DTOs/CommentDto.cs

View workflow job for this annotation

GitHub Actions / Build & test

Non-nullable property 'Author' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public DateTime CreatedAt { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public int ReplyCount { get; set; }
public IEnumerable<AuthorDto> FirstRepliesAuthors { get; set; } = Enumerable.Empty<AuthorDto>();
}
13 changes: 13 additions & 0 deletions src/CrowdParlay.Social.Application/DTOs/Page.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace CrowdParlay.Social.Application.DTOs;

public class Page<T>
{
public required int TotalCount { get; set; }
public required IEnumerable<T> Items { get; set; }

public static Page<T> Empty => new()
{
TotalCount = 0,
Items = Enumerable.Empty<T>()
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using CrowdParlay.Social.Infrastructure.Persistence.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Neo4jClient;
using Neo4j.Driver;

namespace CrowdParlay.Social.Infrastructure.Persistence;

Expand Down Expand Up @@ -30,7 +30,7 @@ private static IServiceCollection AddNeo4j(this IServiceCollection services, ICo
configuration["NEO4J_PASSWORD"] ??
throw new InvalidOperationException("NEO4J_PASSWORD is not set!");

var client = new BoltGraphClient(uri, username, password);
return services.AddSingleton<IGraphClient>(client);
var driver = GraphDatabase.Driver(uri, AuthTokens.Basic(username, password));
return services.AddSingleton(driver);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageReference Include="Neo4jClient" Version="5.1.11" />
<PackageReference Include="Neo4j.Driver" Version="5.17.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,89 +1,127 @@
using CrowdParlay.Social.Application.Abstractions;
using CrowdParlay.Social.Application.DTOs;
using CrowdParlay.Social.Application.Exceptions;
using Neo4jClient;
using Mapster;
using Neo4j.Driver;

namespace CrowdParlay.Social.Infrastructure.Persistence.Services;

public class AuthorRepository : IAuthorRepository
{
private readonly IGraphClient _graphClient;
private readonly IDriver _driver;

public AuthorRepository(IGraphClient graphClient) => _graphClient = graphClient;
public AuthorRepository(IDriver driver) => _driver = driver;

public async Task<AuthorDto> GetByIdAsync(Guid id)
{
var results = await _graphClient.Cypher
.WithParams(new { id })
.Match("(a:Author { Id: $id })")
.Return<AuthorDto>("a")
.ResultsAsync;
await using var session = _driver.AsyncSession();
return await session.ExecuteReadAsync(async runner =>
{
var data = await runner.RunAsync(
"""
MATCH (author:Author { Id: $id })
RETURN {
Id: author.Id,
Username: author.Username,
DisplayName: author.DisplayName,
AvatarUrl: author.AvatarUrl
}
""",
new { id = id.ToString() });

return
results.SingleOrDefault()
?? throw new NotFoundException();
var record = await data.SingleAsync();
return record[0].Adapt<AuthorDto>();
});
}

public async Task<AuthorDto> CreateAsync(Guid id, string username, string displayName, string? avatarUrl)
{
var results = await _graphClient.Cypher
.WithParams(new
{
id,
username,
displayName,
avatarUrl
})
.Create(
await using var session = _driver.AsyncSession();
return await session.ExecuteWriteAsync(async runner =>
{
var data = await runner.RunAsync(
"""
(a:Author {
CREATE (author:Author {
Id: $id,
Username: $username,
DisplayName: $displayName,
AvatarUrl: $avatarUrl
})
""")
.Return<AuthorDto>("a")
.ResultsAsync;
RETURN {
Id: author.Id,
Username: author.Username,
DisplayName: author.DisplayName,
AvatarUrl: author.AvatarUrl
}
""",
new
{
id = id.ToString(),
username,
displayName,
avatarUrl
});

return results.Single();
var record = await data.SingleAsync();
return record[0].Adapt<AuthorDto>();
});
}

public async Task<AuthorDto> UpdateAsync(Guid id, string username, string displayName, string? avatarUrl)
{
var results = await _graphClient.Cypher
.WithParams(new
{
id,
username,
displayName,
avatarUrl
})
.Match("(a:Author { Id: $id })")
.Set(
await using var session = _driver.AsyncSession();
return await session.ExecuteWriteAsync(async runner =>
{
var data = await runner.RunAsync(
"""
a.Username = $username,
a.DisplayName = $displayName,
a.AvatarUrl = $avatarUrl
""")
.Return<AuthorDto>("a")
.ResultsAsync;
CREATE (author:Author {
Id: $id,
Username: $username,
DisplayName: $displayName,
AvatarUrl: $avatarUrl
})
MATCH (author:Author { Id: $id })
SET author.Username = $username,
author.DisplayName = $displayName,
author.AvatarUrl = $avatarUrl
RETURN {
Id: author.Id,
Username: author.Username,
DisplayName: author.DisplayName,
AvatarUrl: author.AvatarUrl
}
""",
new
{
id = id.ToString(),
username,
displayName,
avatarUrl
});

return
results.SingleOrDefault()
?? throw new NotFoundException();
var record = await data.SingleAsync();
return record[0].Adapt<AuthorDto>();
});
}

public async Task DeleteAsync(Guid id)
{
var results = await _graphClient.Cypher
.WithParams(new { id })
.OptionalMatch("(a:Author { Id: $id })")
.Delete("a")
.Return<bool>("COUNT(a) = 0")
.ResultsAsync;
await using var session = _driver.AsyncSession();
var notFount = await session.ExecuteWriteAsync(async runner =>
{
var data = await runner.RunAsync(
"""
OPTIONAL MATCH (author:Author { Id: $id })
DETACH DELETE author
RETURN COUNT(author) = 0
""",
new { id = id.ToString() });

var record = await data.SingleAsync();
return record[0].As<bool>();
});

if (results.Single())
if (notFount)
throw new NotFoundException();
}
}
Loading
Loading