From 5644aa0ebf7cc3ce4b098770a94551c63234d04e Mon Sep 17 00:00:00 2001 From: Yui Date: Wed, 26 Nov 2025 16:50:06 -0300 Subject: [PATCH] initial commit --- .gitignore | 5 ++ .idea/.idea.GraphQLTEST/.idea/.gitignore | 15 ++++ .../.idea/copilot.data.migration.agent.xml | 6 ++ .idea/.idea.GraphQLTEST/.idea/encodings.xml | 4 ++ .idea/.idea.GraphQLTEST/.idea/indexLayout.xml | 8 +++ .idea/.idea.GraphQLTEST/.idea/vcs.xml | 6 ++ Application/Application.csproj | 18 +++++ Application/DependencyInjection.cs | 12 ++++ Application/Exceptions/BaseException.cs | 8 +++ .../Exceptions/DuplicateEmailException.cs | 8 +++ .../Exceptions/DuplicateUsernameException.cs | 8 +++ .../InsufficientParametersException.cs | 8 +++ .../Exceptions/UserNotFoundException.cs | 8 +++ .../Repositories/IKillRepository.cs | 12 ++++ .../Repositories/IUserRepository.cs | 11 +++ .../Interfaces/Services/IKillService.cs | 14 ++++ .../Interfaces/Services/IUserService.cs | 12 ++++ Application/Services/KillService.cs | 48 +++++++++++++ Application/Services/UserService.cs | 71 +++++++++++++++++++ Domain/Domain.csproj | 10 +++ Domain/Entities/Kill.cs | 10 +++ Domain/Entities/User.cs | 11 +++ Domain/Enums/EErrorCategory.cs | 8 +++ .../ErrorCategories/EUserValidationError.cs | 7 ++ GraphQLTEST.sln | 34 +++++++++ GraphQLTEST.sln.DotSettings.user | 5 ++ Infrastructure/Database/AppDbContext.cs | 12 ++++ Infrastructure/Database/DbSeeder.cs | 42 +++++++++++ Infrastructure/DependencyInjection.cs | 23 ++++++ Infrastructure/Infrastructure.csproj | 21 ++++++ Infrastructure/Repositories/KillRepository.cs | 34 +++++++++ Infrastructure/Repositories/UserRepository.cs | 33 +++++++++ .../GraphQL/DataLoaders/KillsDataLoader.cs | 28 ++++++++ .../ErrorFilters/LoggingErrorFilter.cs | 12 ++++ .../ErrorFilters/UserFriendlyErrorFilter.cs | 23 ++++++ Presentation/GraphQL/MutationType.cs | 6 ++ .../GraphQL/Mutations/KillMutations.cs | 17 +++++ .../GraphQL/Mutations/UserMutations.cs | 32 +++++++++ Presentation/GraphQL/Queries/KillQueries.cs | 7 ++ Presentation/GraphQL/Queries/UserQueries.cs | 13 ++++ Presentation/GraphQL/SubscriptionType.cs | 6 ++ .../Subscriptions/UserSubscriptions.cs | 13 ++++ Presentation/GraphQL/Types/KillNode.cs | 12 ++++ Presentation/GraphQL/Types/UserNode.cs | 26 +++++++ Presentation/Presentation.csproj | 30 ++++++++ Presentation/Program.cs | 42 +++++++++++ Presentation/Properties/ModuleInfo.cs | 1 + 47 files changed, 800 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.idea.GraphQLTEST/.idea/.gitignore create mode 100644 .idea/.idea.GraphQLTEST/.idea/copilot.data.migration.agent.xml create mode 100644 .idea/.idea.GraphQLTEST/.idea/encodings.xml create mode 100644 .idea/.idea.GraphQLTEST/.idea/indexLayout.xml create mode 100644 .idea/.idea.GraphQLTEST/.idea/vcs.xml create mode 100644 Application/Application.csproj create mode 100644 Application/DependencyInjection.cs create mode 100644 Application/Exceptions/BaseException.cs create mode 100644 Application/Exceptions/DuplicateEmailException.cs create mode 100644 Application/Exceptions/DuplicateUsernameException.cs create mode 100644 Application/Exceptions/InsufficientParametersException.cs create mode 100644 Application/Exceptions/UserNotFoundException.cs create mode 100644 Application/Interfaces/Repositories/IKillRepository.cs create mode 100644 Application/Interfaces/Repositories/IUserRepository.cs create mode 100644 Application/Interfaces/Services/IKillService.cs create mode 100644 Application/Interfaces/Services/IUserService.cs create mode 100644 Application/Services/KillService.cs create mode 100644 Application/Services/UserService.cs create mode 100644 Domain/Domain.csproj create mode 100644 Domain/Entities/Kill.cs create mode 100644 Domain/Entities/User.cs create mode 100644 Domain/Enums/EErrorCategory.cs create mode 100644 Domain/Enums/ErrorCategories/EUserValidationError.cs create mode 100644 GraphQLTEST.sln create mode 100644 GraphQLTEST.sln.DotSettings.user create mode 100644 Infrastructure/Database/AppDbContext.cs create mode 100644 Infrastructure/Database/DbSeeder.cs create mode 100644 Infrastructure/DependencyInjection.cs create mode 100644 Infrastructure/Infrastructure.csproj create mode 100644 Infrastructure/Repositories/KillRepository.cs create mode 100644 Infrastructure/Repositories/UserRepository.cs create mode 100644 Presentation/GraphQL/DataLoaders/KillsDataLoader.cs create mode 100644 Presentation/GraphQL/ErrorFilters/LoggingErrorFilter.cs create mode 100644 Presentation/GraphQL/ErrorFilters/UserFriendlyErrorFilter.cs create mode 100644 Presentation/GraphQL/MutationType.cs create mode 100644 Presentation/GraphQL/Mutations/KillMutations.cs create mode 100644 Presentation/GraphQL/Mutations/UserMutations.cs create mode 100644 Presentation/GraphQL/Queries/KillQueries.cs create mode 100644 Presentation/GraphQL/Queries/UserQueries.cs create mode 100644 Presentation/GraphQL/SubscriptionType.cs create mode 100644 Presentation/GraphQL/Subscriptions/UserSubscriptions.cs create mode 100644 Presentation/GraphQL/Types/KillNode.cs create mode 100644 Presentation/GraphQL/Types/UserNode.cs create mode 100644 Presentation/Presentation.csproj create mode 100644 Presentation/Program.cs create mode 100644 Presentation/Properties/ModuleInfo.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/.idea/.idea.GraphQLTEST/.idea/.gitignore b/.idea/.idea.GraphQLTEST/.idea/.gitignore new file mode 100644 index 0000000..76b0c36 --- /dev/null +++ b/.idea/.idea.GraphQLTEST/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/.idea.GraphQLTEST.iml +/projectSettingsUpdater.xml +/modules.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/.idea.GraphQLTEST/.idea/copilot.data.migration.agent.xml b/.idea/.idea.GraphQLTEST/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000..4ea72a9 --- /dev/null +++ b/.idea/.idea.GraphQLTEST/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/.idea.GraphQLTEST/.idea/encodings.xml b/.idea/.idea.GraphQLTEST/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.GraphQLTEST/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.GraphQLTEST/.idea/indexLayout.xml b/.idea/.idea.GraphQLTEST/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.GraphQLTEST/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.GraphQLTEST/.idea/vcs.xml b/.idea/.idea.GraphQLTEST/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.GraphQLTEST/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Application/Application.csproj b/Application/Application.csproj new file mode 100644 index 0000000..d9c151d --- /dev/null +++ b/Application/Application.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + latest + enable + enable + + + + + + + + + + + diff --git a/Application/DependencyInjection.cs b/Application/DependencyInjection.cs new file mode 100644 index 0000000..0ddd888 --- /dev/null +++ b/Application/DependencyInjection.cs @@ -0,0 +1,12 @@ +using Application.Interfaces.Services; +using Application.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplication(this IServiceCollection services) => + services.AddScoped() + .AddScoped(); +} \ No newline at end of file diff --git a/Application/Exceptions/BaseException.cs b/Application/Exceptions/BaseException.cs new file mode 100644 index 0000000..b2563b0 --- /dev/null +++ b/Application/Exceptions/BaseException.cs @@ -0,0 +1,8 @@ +using Domain.Enums; + +namespace Application.Exceptions; + +public class BaseException(EErrorCategory category, string message) : Exception(message) +{ + public readonly EErrorCategory Category = category; +} \ No newline at end of file diff --git a/Application/Exceptions/DuplicateEmailException.cs b/Application/Exceptions/DuplicateEmailException.cs new file mode 100644 index 0000000..e0a8568 --- /dev/null +++ b/Application/Exceptions/DuplicateEmailException.cs @@ -0,0 +1,8 @@ +using Domain.Enums; + +namespace Application.Exceptions; + +public class DuplicateEmailException(string email) : BaseException(EErrorCategory.UserValidationError, $"Email {email} already registered") +{ + +} \ No newline at end of file diff --git a/Application/Exceptions/DuplicateUsernameException.cs b/Application/Exceptions/DuplicateUsernameException.cs new file mode 100644 index 0000000..d346a68 --- /dev/null +++ b/Application/Exceptions/DuplicateUsernameException.cs @@ -0,0 +1,8 @@ +using Domain.Enums; + +namespace Application.Exceptions; + +public class DuplicateUsernameException(string? username) : BaseException(EErrorCategory.UserValidationError, $"Username {username} is taken") +{ + +} \ No newline at end of file diff --git a/Application/Exceptions/InsufficientParametersException.cs b/Application/Exceptions/InsufficientParametersException.cs new file mode 100644 index 0000000..828cb28 --- /dev/null +++ b/Application/Exceptions/InsufficientParametersException.cs @@ -0,0 +1,8 @@ +using Domain.Enums; + +namespace Application.Exceptions; + +public class InsufficientParametersException(List missingParameters) : BaseException(EErrorCategory.RequestError, "Insufficient Parameters: " + string.Join(", ", missingParameters)) +{ + +} \ No newline at end of file diff --git a/Application/Exceptions/UserNotFoundException.cs b/Application/Exceptions/UserNotFoundException.cs new file mode 100644 index 0000000..8e1f779 --- /dev/null +++ b/Application/Exceptions/UserNotFoundException.cs @@ -0,0 +1,8 @@ +using Domain.Enums; + +namespace Application.Exceptions; + +public class UserNotFoundException(int id) : BaseException(EErrorCategory.DataNotFoundError,$"User with ID {id} not found.") +{ + +} \ No newline at end of file diff --git a/Application/Interfaces/Repositories/IKillRepository.cs b/Application/Interfaces/Repositories/IKillRepository.cs new file mode 100644 index 0000000..8087b96 --- /dev/null +++ b/Application/Interfaces/Repositories/IKillRepository.cs @@ -0,0 +1,12 @@ +using Domain.Entities; + +namespace Application.Interfaces.Repositories; + +public interface IKillRepository +{ + public IEnumerable GetAllKills(); + + public bool TryGetKillById(int id, out Kill? kill); + public void AddKill(Kill kill); + public bool RemoveKill(Kill kill); +} \ No newline at end of file diff --git a/Application/Interfaces/Repositories/IUserRepository.cs b/Application/Interfaces/Repositories/IUserRepository.cs new file mode 100644 index 0000000..539e8a3 --- /dev/null +++ b/Application/Interfaces/Repositories/IUserRepository.cs @@ -0,0 +1,11 @@ +using Domain.Entities; + +namespace Application.Interfaces.Repositories; + +public interface IUserRepository +{ + public IEnumerable GetAllUsers(); + public bool TryGetUserById(int id, out User? user); + public bool AddUser(User user); + public bool UpdateUser(User user); +} \ No newline at end of file diff --git a/Application/Interfaces/Services/IKillService.cs b/Application/Interfaces/Services/IKillService.cs new file mode 100644 index 0000000..9ac9d20 --- /dev/null +++ b/Application/Interfaces/Services/IKillService.cs @@ -0,0 +1,14 @@ +using Application.Interfaces.Repositories; +using Domain.Entities; + +namespace Application.Interfaces.Services; + +public interface IKillService +{ + public IEnumerable GetAllKills(); + public Kill GetKillById(int id); + public IEnumerable GetKillsByUserId(int userId); + public IEnumerable GetKillsByUserId(IEnumerable userIds); + public bool AddKill(Kill kill); + public bool RemoveKill(Kill kill); +} \ No newline at end of file diff --git a/Application/Interfaces/Services/IUserService.cs b/Application/Interfaces/Services/IUserService.cs new file mode 100644 index 0000000..f4e2b3b --- /dev/null +++ b/Application/Interfaces/Services/IUserService.cs @@ -0,0 +1,12 @@ +using Application.Interfaces.Repositories; +using Domain.Entities; + +namespace Application.Interfaces.Services; + +public interface IUserService +{ + public IEnumerable GetAllUsers(); + public User GetUserById(int id); + public bool CreateUser(string username, string password, string email); + public bool UpdateUser(int id, string username, string password, string email); +} \ No newline at end of file diff --git a/Application/Services/KillService.cs b/Application/Services/KillService.cs new file mode 100644 index 0000000..e8c2e7b --- /dev/null +++ b/Application/Services/KillService.cs @@ -0,0 +1,48 @@ +using Application.Interfaces.Repositories; +using Application.Interfaces.Services; +using Domain.Entities; + +namespace Application.Services; + +public class KillService(IKillRepository repository, IUserService userService) : IKillService +{ + private IKillRepository Repository { get; } = repository; + + public IEnumerable GetAllKills() + { + return Repository.GetAllKills(); + } + + public Kill GetKillById(int id) + { + if (!Repository.TryGetKillById(id, out var kill)) + { + throw new KeyNotFoundException($"Failed to find kill with id: {id}"); + } + + return kill; + } + + public IEnumerable GetKillsByUserId(int userId) + { + return Repository.GetAllKills().Where(x => x.UserId == userId); + } + + public IEnumerable GetKillsByUserId(IEnumerable userIds) + { + return Repository.GetAllKills().Where(x => userIds.Contains(x.UserId)); + } + + public bool AddKill(Kill kill) + { + Repository.AddKill(kill); + var user = userService.GetUserById(kill.UserId); + user.Kills.Append(kill); + return true; + } + + public bool RemoveKill(Kill kill) + { + return Repository.RemoveKill(kill); + } +} \ No newline at end of file diff --git a/Application/Services/UserService.cs b/Application/Services/UserService.cs new file mode 100644 index 0000000..5cda416 --- /dev/null +++ b/Application/Services/UserService.cs @@ -0,0 +1,71 @@ +using Application.Exceptions; +using Application.Interfaces.Repositories; +using Application.Interfaces.Services; +using Domain.Entities; + +namespace Application.Services; + +public class UserService(IUserRepository repository) : IUserService +{ + private IUserRepository Repository { get; } = repository; + + public IEnumerable GetAllUsers() + { + return Repository.GetAllUsers(); + } + + public User GetUserById(int id) + { + if (!Repository.TryGetUserById(id, out var user)) + { + throw new ArgumentException($"User with id {id} not found"); + } + + return user; + } + + public bool CreateUser(string username, string password, string email) + { + if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(email)) + { + throw new InsufficientParametersException(["username", "password", "email"]); + } + + if (Repository.GetAllUsers().Any(x => x.Username == username)) + { + throw new DuplicateUsernameException(username); + } + + if (Repository.GetAllUsers().Any(x => x.Email == email)) + { + throw new DuplicateEmailException(email); + } + + var user = new User + { + Id = Repository.GetAllUsers().Count() + 1, + Username = username, + Password = password, + Email = email + }; + return Repository.AddUser(user); + } + + public bool UpdateUser(int id, string username, string password, string email) + { + if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(email)) + { + throw new InsufficientParametersException(["username", "password", "email"]); + } + + if (!Repository.TryGetUserById(id, out var user)) + { + throw new UserNotFoundException(id); + } + + user.Email = email; + user.Password = password; + user.Username = username; + return Repository.UpdateUser(user); + } +} \ No newline at end of file diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj new file mode 100644 index 0000000..5b9f85a --- /dev/null +++ b/Domain/Domain.csproj @@ -0,0 +1,10 @@ + + + + net10.0 + latest + enable + enable + + + diff --git a/Domain/Entities/Kill.cs b/Domain/Entities/Kill.cs new file mode 100644 index 0000000..fdfab8c --- /dev/null +++ b/Domain/Entities/Kill.cs @@ -0,0 +1,10 @@ +namespace Domain.Entities; + +public class Kill +{ + public int Id { get; set; } + public int UserId { get; set; } + public User User { get; set; } + public string Victim { get; set; } + public double Damage { get; set; } +} \ No newline at end of file diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs new file mode 100644 index 0000000..a167bff --- /dev/null +++ b/Domain/Entities/User.cs @@ -0,0 +1,11 @@ +namespace Domain.Entities; + +public class User +{ + public int Id { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string Email { get; set; } + + public IEnumerable Kills { get; set; } = new List(); +} \ No newline at end of file diff --git a/Domain/Enums/EErrorCategory.cs b/Domain/Enums/EErrorCategory.cs new file mode 100644 index 0000000..12c7c0d --- /dev/null +++ b/Domain/Enums/EErrorCategory.cs @@ -0,0 +1,8 @@ +namespace Domain.Enums; + +public enum EErrorCategory +{ + UserValidationError, + DataNotFoundError, + RequestError, +} \ No newline at end of file diff --git a/Domain/Enums/ErrorCategories/EUserValidationError.cs b/Domain/Enums/ErrorCategories/EUserValidationError.cs new file mode 100644 index 0000000..d770d67 --- /dev/null +++ b/Domain/Enums/ErrorCategories/EUserValidationError.cs @@ -0,0 +1,7 @@ +namespace Domain.Enums.ErrorCategories; + +public enum EUserValidationError +{ + DuplicateUserName, + DuplicateEmail, +} \ No newline at end of file diff --git a/GraphQLTEST.sln b/GraphQLTEST.sln new file mode 100644 index 0000000..1e4ef5e --- /dev/null +++ b/GraphQLTEST.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Presentation", "Presentation\Presentation.csproj", "{19ACF9DE-92C8-4F93-AD6D-30C68DB5C002}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "Domain\Domain.csproj", "{F9677CF6-6EBB-49D5-B9CA-822B1F5C681F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "Application\Application.csproj", "{D4834024-39F8-44A5-8FBE-99A2D4D64992}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{8E0F3BFD-CC6E-4FE0-B6F8-65427C3B8602}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {19ACF9DE-92C8-4F93-AD6D-30C68DB5C002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19ACF9DE-92C8-4F93-AD6D-30C68DB5C002}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19ACF9DE-92C8-4F93-AD6D-30C68DB5C002}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19ACF9DE-92C8-4F93-AD6D-30C68DB5C002}.Release|Any CPU.Build.0 = Release|Any CPU + {F9677CF6-6EBB-49D5-B9CA-822B1F5C681F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9677CF6-6EBB-49D5-B9CA-822B1F5C681F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9677CF6-6EBB-49D5-B9CA-822B1F5C681F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9677CF6-6EBB-49D5-B9CA-822B1F5C681F}.Release|Any CPU.Build.0 = Release|Any CPU + {D4834024-39F8-44A5-8FBE-99A2D4D64992}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4834024-39F8-44A5-8FBE-99A2D4D64992}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4834024-39F8-44A5-8FBE-99A2D4D64992}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4834024-39F8-44A5-8FBE-99A2D4D64992}.Release|Any CPU.Build.0 = Release|Any CPU + {8E0F3BFD-CC6E-4FE0-B6F8-65427C3B8602}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E0F3BFD-CC6E-4FE0-B6F8-65427C3B8602}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E0F3BFD-CC6E-4FE0-B6F8-65427C3B8602}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E0F3BFD-CC6E-4FE0-B6F8-65427C3B8602}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/GraphQLTEST.sln.DotSettings.user b/GraphQLTEST.sln.DotSettings.user new file mode 100644 index 0000000..2ce3a10 --- /dev/null +++ b/GraphQLTEST.sln.DotSettings.user @@ -0,0 +1,5 @@ + + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded \ No newline at end of file diff --git a/Infrastructure/Database/AppDbContext.cs b/Infrastructure/Database/AppDbContext.cs new file mode 100644 index 0000000..81d7e17 --- /dev/null +++ b/Infrastructure/Database/AppDbContext.cs @@ -0,0 +1,12 @@ +using Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure.Database; + +public class AppDbContext : DbContext +{ + public DbSet Users { get; set; } + public DbSet Kills { get; set; } + + public AppDbContext(DbContextOptions options) : base(options){} +} \ No newline at end of file diff --git a/Infrastructure/Database/DbSeeder.cs b/Infrastructure/Database/DbSeeder.cs new file mode 100644 index 0000000..71fff2c --- /dev/null +++ b/Infrastructure/Database/DbSeeder.cs @@ -0,0 +1,42 @@ +using Bogus; +using Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure.Database; + +public class DbSeeder +{ + public static async Task SeedAsync(DbContext dbContext, CancellationToken cancellationToken) + { + var exampleUser = await dbContext.Set().FirstOrDefaultAsync(u => u.Email.EndsWith("@example.com"), cancellationToken); + if (exampleUser == null) + { + dbContext.Set().AddRange([ + new User(){Id = 1, Email = "sunpy@example.com", Password = "iAmJaneEldenRing21", Username = "sunpy"}, + new User(){Id = 2, Email = "yui@example.com", Password = "ILoveHalfLife98", Username = "yui"}, + new User(){Id = 3, Email = "johnny@example.com", Password = "iAmCyberPunk77", Username = "johnny"}, + new User(){Id = 4, Email = "skibidi@example.com", Password = "iHateToilets67", Username = "skibidi_hater"}, + ]); + await dbContext.SaveChangesAsync(cancellationToken); + } + var exampleKill = dbContext.Set().FirstOrDefault(k => k.UserId == 1); + if (exampleKill == null) + { + var killSet = dbContext.Set(); + var fake = new Faker() + .RuleFor(k => k.Id, f => ++f.IndexVariable) + .RuleFor(k => k.User, f => f.PickRandom(dbContext.Set().ToList())) + .RuleFor(k => k.UserId, (_, k) => k.User.Id) + .RuleFor(k => k.Damage, f => f.Random.Double(1, 100)) + .RuleFor(k => k.Victim, f => f.Name.FirstName()); + var fakeKills = fake.Generate(500).ToList(); + await killSet.AddRangeAsync(fakeKills, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + } + } + + public static void Seed(DbContext dbContext) + { + Task.Run(() => SeedAsync(dbContext, CancellationToken.None)).Wait(); + } +} \ No newline at end of file diff --git a/Infrastructure/DependencyInjection.cs b/Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..b6be043 --- /dev/null +++ b/Infrastructure/DependencyInjection.cs @@ -0,0 +1,23 @@ +using Application.Interfaces.Repositories; +using Infrastructure.Database; +using Infrastructure.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services) + { + return services.AddDbContextFactory(options => + options + .UseInMemoryDatabase("Skibibase") + .UseSeeding((c, _) => DbSeeder.Seed(c)) + .UseAsyncSeeding((async (context, _, token) => await DbSeeder.SeedAsync(context, token))) + .EnableSensitiveDataLogging() + ) + .AddScoped() + .AddScoped(); + } +} \ No newline at end of file diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj new file mode 100644 index 0000000..4779246 --- /dev/null +++ b/Infrastructure/Infrastructure.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + latest + enable + enable + + + + + + + + + + + + + + diff --git a/Infrastructure/Repositories/KillRepository.cs b/Infrastructure/Repositories/KillRepository.cs new file mode 100644 index 0000000..8c4cee3 --- /dev/null +++ b/Infrastructure/Repositories/KillRepository.cs @@ -0,0 +1,34 @@ +using Application.Interfaces.Repositories; +using Domain.Entities; +using Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure.Repositories; + +public class KillRepository(AppDbContext context) : IKillRepository +{ + public IEnumerable GetAllKills() + { + context.Database.EnsureCreated(); + return context.Kills.AsNoTracking(); + } + + public bool TryGetKillById(int id, out Kill? kill) + { + kill = context.Kills.FirstOrDefault(k => k.Id == id); + + return kill != null; + } + + public void AddKill(Kill kill) + { + context.Kills.Add(kill); + context.SaveChanges(); + } + + public bool RemoveKill(Kill kill) + { + context.Kills.Remove(kill); + return context.SaveChanges() > 0; + } +} \ No newline at end of file diff --git a/Infrastructure/Repositories/UserRepository.cs b/Infrastructure/Repositories/UserRepository.cs new file mode 100644 index 0000000..1b1ffba --- /dev/null +++ b/Infrastructure/Repositories/UserRepository.cs @@ -0,0 +1,33 @@ +using Application.Interfaces.Repositories; +using Domain.Entities; +using Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure.Repositories; + +public class UserRepository(AppDbContext context) : IUserRepository +{ + public IEnumerable GetAllUsers() + { + context.Database.EnsureCreated(); + return context.Users.AsNoTracking(); + } + + public bool TryGetUserById(int id, out User? user) + { + user = context.Users.FirstOrDefault(u => u.Id == id); + return user != null; + } + + public bool AddUser(User user) + { + context.Users.Add(user); + return context.SaveChanges() != 0; + } + + public bool UpdateUser(User user) + { + context.Users.Update(user); + return context.SaveChanges() != 0; + } +} \ No newline at end of file diff --git a/Presentation/GraphQL/DataLoaders/KillsDataLoader.cs b/Presentation/GraphQL/DataLoaders/KillsDataLoader.cs new file mode 100644 index 0000000..9fdc292 --- /dev/null +++ b/Presentation/GraphQL/DataLoaders/KillsDataLoader.cs @@ -0,0 +1,28 @@ +using Application.Interfaces.Services; +using Domain.Entities; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace GraphQLTEST.GraphQL.DataLoaders; + +public class KillsByUserDataLoader : GroupedDataLoader{ + private readonly IServiceProvider _serviceProvider; + + public KillsByUserDataLoader( + IServiceProvider serviceProvider, + IBatchScheduler batchScheduler, + DataLoaderOptions options): base(batchScheduler, options){ + _serviceProvider = serviceProvider; + } + + protected override async Task> LoadGroupedBatchAsync(IReadOnlyList keys, CancellationToken cancellationToken) + { + await using var scope = _serviceProvider.CreateAsyncScope(); + var service = scope.ServiceProvider.GetRequiredService(); + + return await service.GetKillsByUserId(keys) + .ToAsyncEnumerable() + .ToLookupAsync(k => k.UserId, cancellationToken: cancellationToken); + } +} \ No newline at end of file diff --git a/Presentation/GraphQL/ErrorFilters/LoggingErrorFilter.cs b/Presentation/GraphQL/ErrorFilters/LoggingErrorFilter.cs new file mode 100644 index 0000000..6cffd16 --- /dev/null +++ b/Presentation/GraphQL/ErrorFilters/LoggingErrorFilter.cs @@ -0,0 +1,12 @@ +using HotChocolate.Execution; + +namespace GraphQLTEST.GraphQL.ErrorFilters; + +public class LoggingErrorFilter : IErrorFilter +{ + public IError OnError(IError error) + { + Console.WriteLine(error.Exception?.ToString() ?? error.Message); + return error; + } +} \ No newline at end of file diff --git a/Presentation/GraphQL/ErrorFilters/UserFriendlyErrorFilter.cs b/Presentation/GraphQL/ErrorFilters/UserFriendlyErrorFilter.cs new file mode 100644 index 0000000..16c9be9 --- /dev/null +++ b/Presentation/GraphQL/ErrorFilters/UserFriendlyErrorFilter.cs @@ -0,0 +1,23 @@ +using System.Collections.ObjectModel; +using Application.Exceptions; +using HotChocolate.Execution; + +namespace GraphQLTEST.GraphQL.ErrorFilters; + +public class UserFriendlyErrorFilter : IErrorFilter +{ + public IError OnError(IError error) + { + if (error.Exception is BaseException baseException) + { + return ErrorBuilder + .FromError(error) + .SetMessage(baseException?.Message ?? "Unknown Error") + .SetExtension("errorCategory", baseException?.Category) + .SetExtension("errorCode", baseException?.GetType().Name) + .Build(); + } + + return ErrorBuilder.FromError(error).SetMessage(error.Exception?.Message ?? "Unknown Error").Build(); + } +} \ No newline at end of file diff --git a/Presentation/GraphQL/MutationType.cs b/Presentation/GraphQL/MutationType.cs new file mode 100644 index 0000000..ec8babb --- /dev/null +++ b/Presentation/GraphQL/MutationType.cs @@ -0,0 +1,6 @@ +namespace GraphQLTEST.GraphQL; + +public class MutationType +{ + +} \ No newline at end of file diff --git a/Presentation/GraphQL/Mutations/KillMutations.cs b/Presentation/GraphQL/Mutations/KillMutations.cs new file mode 100644 index 0000000..a28e0d8 --- /dev/null +++ b/Presentation/GraphQL/Mutations/KillMutations.cs @@ -0,0 +1,17 @@ +using Application.Interfaces.Services; +using Domain.Entities; +using HotChocolate.Subscriptions; + +namespace GraphQLTEST.GraphQL.Mutations; + +[ExtendObjectType(nameof(MutationType))] +public class KillMutations +{ + public async Task RemoveKill(int id, [Service] IKillService service, [Service] ITopicEventSender eventSender, CancellationToken cancellationToken) + { + var kill = service.GetKillById(id); + service.RemoveKill(kill); + await eventSender.SendAsync(nameof(RemoveKill), kill, cancellationToken); + return kill; + } +} \ No newline at end of file diff --git a/Presentation/GraphQL/Mutations/UserMutations.cs b/Presentation/GraphQL/Mutations/UserMutations.cs new file mode 100644 index 0000000..ad32260 --- /dev/null +++ b/Presentation/GraphQL/Mutations/UserMutations.cs @@ -0,0 +1,32 @@ +using Application; +using Application.Interfaces.Services; +using Application.Services; +using Domain.Entities; +using GraphQLTEST.GraphQL.Subscriptions; +using HotChocolate.Subscriptions; + +namespace GraphQLTEST.GraphQL.Mutations; + +[ExtendObjectType(nameof(MutationType))] +public class UserMutations +{ + public async Task AddUser(string username, string email, string password, [Service] IUserService service) => + await Task.Run(() => service.CreateUser(username, password, email)); + + public async Task AddKill( + string victim, + double damage, + int userId, + [Service] IKillService killService, + [Service] IUserService userService, + [Service] ITopicEventSender eventSender, + CancellationToken cancellationToken) + { + var user = userService.GetUserById(userId); + var kill = new Kill + { Id = Random.Shared.Next(0, 100), Damage = damage, User = user, UserId = user.Id, Victim = victim }; + killService.AddKill(kill); + await eventSender.SendAsync(nameof(UserSubscriptions.KillAdded), kill, cancellationToken); + return kill; + } +} \ No newline at end of file diff --git a/Presentation/GraphQL/Queries/KillQueries.cs b/Presentation/GraphQL/Queries/KillQueries.cs new file mode 100644 index 0000000..1a1e7f1 --- /dev/null +++ b/Presentation/GraphQL/Queries/KillQueries.cs @@ -0,0 +1,7 @@ +namespace GraphQLTEST.GraphQL.Queries; + +[QueryType] +public class KillQueries +{ + +} \ No newline at end of file diff --git a/Presentation/GraphQL/Queries/UserQueries.cs b/Presentation/GraphQL/Queries/UserQueries.cs new file mode 100644 index 0000000..c68b818 --- /dev/null +++ b/Presentation/GraphQL/Queries/UserQueries.cs @@ -0,0 +1,13 @@ +using Application; +using Application.Interfaces.Services; +using Domain.Entities; + +namespace GraphQLTEST.GraphQL.Queries; + +[QueryType] +public class UserQueries +{ + [UseProjection] + [UseFiltering] + public IEnumerable GetUsers([Service] IUserService service) => service.GetAllUsers(); +} \ No newline at end of file diff --git a/Presentation/GraphQL/SubscriptionType.cs b/Presentation/GraphQL/SubscriptionType.cs new file mode 100644 index 0000000..717f6d5 --- /dev/null +++ b/Presentation/GraphQL/SubscriptionType.cs @@ -0,0 +1,6 @@ +namespace GraphQLTEST.GraphQL; + +public class SubscriptionType +{ + +} \ No newline at end of file diff --git a/Presentation/GraphQL/Subscriptions/UserSubscriptions.cs b/Presentation/GraphQL/Subscriptions/UserSubscriptions.cs new file mode 100644 index 0000000..b76507e --- /dev/null +++ b/Presentation/GraphQL/Subscriptions/UserSubscriptions.cs @@ -0,0 +1,13 @@ +using Domain.Entities; +using GraphQLTEST.GraphQL.Mutations; +using HotChocolate.Execution; +using HotChocolate.Subscriptions; + +namespace GraphQLTEST.GraphQL.Subscriptions; + +[ExtendObjectType(nameof(SubscriptionType))] +public class UserSubscriptions +{ + [Subscribe] + public Kill KillAdded([EventMessage] Kill kill) => kill; +} \ No newline at end of file diff --git a/Presentation/GraphQL/Types/KillNode.cs b/Presentation/GraphQL/Types/KillNode.cs new file mode 100644 index 0000000..99a2fe5 --- /dev/null +++ b/Presentation/GraphQL/Types/KillNode.cs @@ -0,0 +1,12 @@ +using Domain.Entities; + +namespace GraphQLTEST.GraphQL.Types; + +public class KillNode : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(x => x.Id); + descriptor.Ignore(x => x.UserId); + } +} \ No newline at end of file diff --git a/Presentation/GraphQL/Types/UserNode.cs b/Presentation/GraphQL/Types/UserNode.cs new file mode 100644 index 0000000..69f748f --- /dev/null +++ b/Presentation/GraphQL/Types/UserNode.cs @@ -0,0 +1,26 @@ +using System.Security.Principal; +using Application.Interfaces.Services; +using Domain.Entities; +using GraphQLTEST.GraphQL.DataLoaders; +using GreenDonut.Data; +using HotChocolate.Types.Pagination; + +namespace GraphQLTEST.GraphQL.Types; + +public class UserNode : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(x => x.Id); + descriptor.Ignore(x => x.Password); + descriptor.Field(x => x.Kills) + .UsePaging() + .UseFiltering() + .ParentRequires(x => nameof(x.Id)) + .Resolve(async (r, t) => + { + User parent = r.Parent(); + return await r.DataLoader().LoadAsync(parent.Id, t); + }); + } +} \ No newline at end of file diff --git a/Presentation/Presentation.csproj b/Presentation/Presentation.csproj new file mode 100644 index 0000000..d126303 --- /dev/null +++ b/Presentation/Presentation.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + latest + enable + enable + GraphQLTEST + Exe + + + + + + + + + + + + + + + + + + + + + diff --git a/Presentation/Program.cs b/Presentation/Program.cs new file mode 100644 index 0000000..869c1fd --- /dev/null +++ b/Presentation/Program.cs @@ -0,0 +1,42 @@ +using Application; +using GraphQLTEST.GraphQL; +using GraphQLTEST.GraphQL.ErrorFilters; +using GraphQLTEST.GraphQL.Mutations; +using Infrastructure; +using Infrastructure.Database; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddApplication(); +builder.Services.AddInfrastructure(); + +builder + .Services.AddGraphQLServer() + .AddMutationType() + .AddSubscriptionType() + .AddInMemorySubscriptions() + .AddTypes() + .AddPagingArguments() + .AddQueryContext() + .AddSorting() + .AddFiltering() + .AddProjections() + .AddErrorFilter() + .AddErrorFilter() + .AddMutationConventions( + new MutationConventionOptions + { + InputArgumentName = "input", + InputTypeNamePattern = "{MutationName}Input", + PayloadTypeNamePattern = "{MutationName}Payload", + PayloadErrorTypeNamePattern = "{MutationName}Error", + PayloadErrorsFieldName = "errors", + ApplyToAllMutations = true, + }); +var app = builder.Build(); + +app.MapGraphQL(); +app.RunWithGraphQLCommands(args); \ No newline at end of file diff --git a/Presentation/Properties/ModuleInfo.cs b/Presentation/Properties/ModuleInfo.cs new file mode 100644 index 0000000..faab7fa --- /dev/null +++ b/Presentation/Properties/ModuleInfo.cs @@ -0,0 +1 @@ +[assembly: Module("Types")] \ No newline at end of file