From 947a5c77ba0a8928334b34ef58e39b6533b2d9bb Mon Sep 17 00:00:00 2001 From: elotoja Date: Tue, 26 Dec 2023 12:53:20 +0100 Subject: [PATCH 1/3] Add API description --- src/Quizer.Api/OpenApi/ConfigureSwaggerOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Quizer.Api/OpenApi/ConfigureSwaggerOptions.cs b/src/Quizer.Api/OpenApi/ConfigureSwaggerOptions.cs index f9bac39..0c1f865 100644 --- a/src/Quizer.Api/OpenApi/ConfigureSwaggerOptions.cs +++ b/src/Quizer.Api/OpenApi/ConfigureSwaggerOptions.cs @@ -19,8 +19,8 @@ public void Configure(SwaggerGenOptions options) description.GroupName, new OpenApiInfo() { - Title = "Example API", - Description = "An example API", + Title = "Quizer API", + Description = "An API for the Quizer App", Version = description.ApiVersion.ToString(), }); } From 46b6fdab7c475330f80016cea9516469615bdf25 Mon Sep 17 00:00:00 2001 From: elotoja Date: Tue, 26 Dec 2023 13:44:11 +0100 Subject: [PATCH 2/3] Add slugify and fix index --- .../Controllers/V1/QuizController.cs | 12 ++-- .../Interfaces/Persistance/IQuizRepository.cs | 2 +- src/Quizer.Application/DependencyInjection.cs | 4 ++ .../Quizer.Application.csproj | 1 + .../CreateQuiz/CreateQuizCommandHandler.cs | 18 ++++- .../UpdateQuiz/UpdateQuizCommandHandler.cs | 9 ++- .../Quizes/Queries/GetQuiz/GetQuizQuery.cs | 9 --- .../Queries/GetQuiz/GetQuizQueryHandler.cs | 35 --------- .../Queries/GetQuizById/GetQuizByIdQuery.cs | 8 +++ .../GetQuizById/GetQuizByIdQueryHandler.cs | 24 +++++++ .../GetQuizByName/GetQuizByNameQuery.cs | 9 +++ .../GetQuizByNameQueryHandler.cs | 26 +++++++ .../Services/DependencyInjection.cs | 17 +++++ .../Services/Slugify/ISlugifyService.cs | 7 ++ .../Services/Slugify/SlugifyService.cs | 71 +++++++++++++++++++ src/Quizer.Domain/QuizAggregate/Quiz.cs | 19 +++-- .../Configuration/QuizConfigurations.cs | 2 +- .../Repositories/QuizRepository.cs | 4 +- 18 files changed, 215 insertions(+), 62 deletions(-) delete mode 100644 src/Quizer.Application/Quizes/Queries/GetQuiz/GetQuizQuery.cs delete mode 100644 src/Quizer.Application/Quizes/Queries/GetQuiz/GetQuizQueryHandler.cs create mode 100644 src/Quizer.Application/Quizes/Queries/GetQuizById/GetQuizByIdQuery.cs create mode 100644 src/Quizer.Application/Quizes/Queries/GetQuizById/GetQuizByIdQueryHandler.cs create mode 100644 src/Quizer.Application/Quizes/Queries/GetQuizByName/GetQuizByNameQuery.cs create mode 100644 src/Quizer.Application/Quizes/Queries/GetQuizByName/GetQuizByNameQueryHandler.cs create mode 100644 src/Quizer.Application/Services/DependencyInjection.cs create mode 100644 src/Quizer.Application/Services/Slugify/ISlugifyService.cs create mode 100644 src/Quizer.Application/Services/Slugify/SlugifyService.cs diff --git a/src/Quizer.Api/Controllers/V1/QuizController.cs b/src/Quizer.Api/Controllers/V1/QuizController.cs index 7ad25b2..4bcc949 100644 --- a/src/Quizer.Api/Controllers/V1/QuizController.cs +++ b/src/Quizer.Api/Controllers/V1/QuizController.cs @@ -5,7 +5,7 @@ using Quizer.Application.Quizes.Commands.CreateQuiz; using Quizer.Application.Quizes.Commands.DeleteQuiz; using Quizer.Application.Quizes.Commands.UpdateQuiz; -using Quizer.Application.Quizes.Queries.GetQuiz; +using Quizer.Application.Quizes.Queries.GetQuizById; using Quizer.Application.Quizes.Queries.GetQuizes; using Quizer.Contracts.Quiz; using System.Security.Claims; @@ -37,11 +37,11 @@ public async Task GetQuizes(string? userId = null) Problem); } - [HttpGet("id/{id:guid}")] + [HttpGet("{id:guid}")] [AllowAnonymous] public async Task GetQuizById(Guid id) { - var query = new GetQuizQuery(id, null); + var query = new GetQuizByIdQuery(id); var result = await _mediator.Send(query); return result.Match( @@ -49,11 +49,11 @@ public async Task GetQuizById(Guid id) Problem); } - [HttpGet("name/{name}")] + [HttpGet("{userName}/{quizName}")] [AllowAnonymous] - public async Task GetQuizByName(string name) + public async Task GetQuizByName(string userName, string quizName) { - var query = new GetQuizQuery(null, name); + var query = new GetQuizByNameQuery(userName, quizName); var result = await _mediator.Send(query); return result.Match( diff --git a/src/Quizer.Application/Common/Interfaces/Persistance/IQuizRepository.cs b/src/Quizer.Application/Common/Interfaces/Persistance/IQuizRepository.cs index 63f6e9a..e00ecd9 100644 --- a/src/Quizer.Application/Common/Interfaces/Persistance/IQuizRepository.cs +++ b/src/Quizer.Application/Common/Interfaces/Persistance/IQuizRepository.cs @@ -7,6 +7,6 @@ public interface IQuizRepository Task Add(Quiz quiz); Task> GetAll(Guid? userId = null); Task Get(QuizId id); - Task Get(string name); + Task Get(string userName, string quizName); void Delete(Quiz quiz); } diff --git a/src/Quizer.Application/DependencyInjection.cs b/src/Quizer.Application/DependencyInjection.cs index c686760..d6da82a 100644 --- a/src/Quizer.Application/DependencyInjection.cs +++ b/src/Quizer.Application/DependencyInjection.cs @@ -2,6 +2,7 @@ using MediatR; using Microsoft.Extensions.DependencyInjection; using Quizer.Application.Common.Behaviors; +using Quizer.Application.Services; using System.Reflection; namespace Quizer.Application; @@ -18,6 +19,9 @@ public static IServiceCollection AddApplication(this IServiceCollection services services.AddScoped(typeof(IPipelineBehavior<,>), typeof(UnitOfWorkBehavior<,>)); services.AddValidatorsFromAssembly(assembly); + + services.AddServices(); + return services; } } diff --git a/src/Quizer.Application/Quizer.Application.csproj b/src/Quizer.Application/Quizer.Application.csproj index 4b86595..3f8bbd2 100644 --- a/src/Quizer.Application/Quizer.Application.csproj +++ b/src/Quizer.Application/Quizer.Application.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Quizer.Application/Quizes/Commands/CreateQuiz/CreateQuizCommandHandler.cs b/src/Quizer.Application/Quizes/Commands/CreateQuiz/CreateQuizCommandHandler.cs index 42f4df9..76d3292 100644 --- a/src/Quizer.Application/Quizes/Commands/CreateQuiz/CreateQuizCommandHandler.cs +++ b/src/Quizer.Application/Quizes/Commands/CreateQuiz/CreateQuizCommandHandler.cs @@ -1,25 +1,35 @@ using ErrorOr; using MediatR; +using Microsoft.AspNetCore.Identity; using Quizer.Application.Common.Interfaces.Persistance; +using Quizer.Application.Services.Slugify; using Quizer.Domain.Common.Errors; using Quizer.Domain.Common.ValueObjects; using Quizer.Domain.QuizAggregate; using Quizer.Domain.QuizAggregate.Entities; +using Quizer.Domain.UserAggregate; namespace Quizer.Application.Quizes.Commands.CreateQuiz; public class CreateQuizCommandHandler : IRequestHandler> { private readonly IQuizRepository _quizRepository; + private readonly ISlugifyService _slugifyService; + private readonly UserManager _userManager; - public CreateQuizCommandHandler(IQuizRepository quizRepository) + public CreateQuizCommandHandler(IQuizRepository quizRepository, ISlugifyService slugifyService, UserManager userManager) { _quizRepository = quizRepository; + _slugifyService = slugifyService; + _userManager = userManager; } public async Task> Handle(CreateQuizCommand request, CancellationToken cancellationToken) { - if ((await _quizRepository.Get(request.Name)) is not null) + var user = await _userManager.FindByIdAsync(request.UserId.ToString()); + string userName = user is null || user.UserName is null ? "Anonymous" : user.UserName; + + if ((await _quizRepository.Get(userName, request.Name)) is not null) return Errors.Quiz.DuplicateName; var errors = new List(); @@ -36,10 +46,14 @@ public async Task> Handle(CreateQuizCommand request, Cancellatio var questions = questionResults .ConvertAll(q => q.Value); + string slug = _slugifyService.GenerateSlug(request.Name); + var result = Quiz.Create( request.Name, + slug, request.Description, request.UserId, + userName, AverageRating.CreateNew(), questions ); diff --git a/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs b/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs index cc0f5a6..8011451 100644 --- a/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs +++ b/src/Quizer.Application/Quizes/Commands/UpdateQuiz/UpdateQuizCommandHandler.cs @@ -2,6 +2,7 @@ using MediatR; using Quizer.Application.Common.Interfaces.Persistance; using Quizer.Application.Quizes.Commands.UpdateQuiz; +using Quizer.Application.Services.Slugify; using Quizer.Domain.Common.Errors; using Quizer.Domain.QuizAggregate; @@ -10,10 +11,12 @@ namespace Quizer.Application.Quizes.Commands.DeleteQuiz; public class UpdateQuizCommandHandler : IRequestHandler> { private readonly IQuizRepository _quizRepository; + private readonly ISlugifyService _slugifyService; - public UpdateQuizCommandHandler(IQuizRepository quizRepository) + public UpdateQuizCommandHandler(IQuizRepository quizRepository, ISlugifyService slugifyService) { _quizRepository = quizRepository; + _slugifyService = slugifyService; } public async Task> Handle(UpdateQuizCommand request, CancellationToken cancellationToken) @@ -25,7 +28,9 @@ public async Task> Handle(UpdateQuizCommand request, Cancellatio var id = (QuizId)quiz.Id; - var result = quiz.Update(request.Name, request.Description); + string slug = _slugifyService.GenerateSlug(request.Name); + + var result = quiz.Update(request.Name, slug, request.Description); if (result.IsError) return result.Errors; return id; diff --git a/src/Quizer.Application/Quizes/Queries/GetQuiz/GetQuizQuery.cs b/src/Quizer.Application/Quizes/Queries/GetQuiz/GetQuizQuery.cs deleted file mode 100644 index 21966ae..0000000 --- a/src/Quizer.Application/Quizes/Queries/GetQuiz/GetQuizQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using ErrorOr; -using MediatR; -using Quizer.Domain.QuizAggregate; - -namespace Quizer.Application.Quizes.Queries.GetQuiz; - -public record GetQuizQuery( - Guid? QuizId, - string? Name) : IRequest>; diff --git a/src/Quizer.Application/Quizes/Queries/GetQuiz/GetQuizQueryHandler.cs b/src/Quizer.Application/Quizes/Queries/GetQuiz/GetQuizQueryHandler.cs deleted file mode 100644 index 73fc356..0000000 --- a/src/Quizer.Application/Quizes/Queries/GetQuiz/GetQuizQueryHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using ErrorOr; -using MediatR; -using Quizer.Application.Common.Interfaces.Persistance; -using Quizer.Domain.Common.Errors; -using Quizer.Domain.QuizAggregate; - -namespace Quizer.Application.Quizes.Queries.GetQuiz; - -public class GetQuizQueryHandler : IRequestHandler> -{ - private readonly IQuizRepository _quizRepository; - - public GetQuizQueryHandler(IQuizRepository quizRepository) - { - _quizRepository = quizRepository; - } - - public async Task> Handle(GetQuizQuery request, CancellationToken cancellationToken) - { - if (request.Name is not null) - { - var quiz = await _quizRepository.Get(request.Name); - if (quiz is null) return Errors.Quiz.NotFound; - return quiz; - } - if (request.QuizId is not null) - { - var quiz = await _quizRepository.Get(QuizId.Create((Guid)request.QuizId)); - if (quiz is null) return Errors.Quiz.NotFound; - return quiz; - } - - return Errors.Quiz.NotFound; - } -} diff --git a/src/Quizer.Application/Quizes/Queries/GetQuizById/GetQuizByIdQuery.cs b/src/Quizer.Application/Quizes/Queries/GetQuizById/GetQuizByIdQuery.cs new file mode 100644 index 0000000..68ec717 --- /dev/null +++ b/src/Quizer.Application/Quizes/Queries/GetQuizById/GetQuizByIdQuery.cs @@ -0,0 +1,8 @@ +using ErrorOr; +using MediatR; +using Quizer.Domain.QuizAggregate; + +namespace Quizer.Application.Quizes.Queries.GetQuizById; + +public record GetQuizByIdQuery( + Guid QuizId) : IRequest>; diff --git a/src/Quizer.Application/Quizes/Queries/GetQuizById/GetQuizByIdQueryHandler.cs b/src/Quizer.Application/Quizes/Queries/GetQuizById/GetQuizByIdQueryHandler.cs new file mode 100644 index 0000000..4f9ecd5 --- /dev/null +++ b/src/Quizer.Application/Quizes/Queries/GetQuizById/GetQuizByIdQueryHandler.cs @@ -0,0 +1,24 @@ +using ErrorOr; +using MediatR; +using Quizer.Application.Common.Interfaces.Persistance; +using Quizer.Domain.Common.Errors; +using Quizer.Domain.QuizAggregate; + +namespace Quizer.Application.Quizes.Queries.GetQuizById; + +public class GetQuizByIdQueryHandler : IRequestHandler> +{ + private readonly IQuizRepository _quizRepository; + + public GetQuizByIdQueryHandler(IQuizRepository quizRepository) + { + _quizRepository = quizRepository; + } + + public async Task> Handle(GetQuizByIdQuery request, CancellationToken cancellationToken) + { + var quiz = await _quizRepository.Get(QuizId.Create(request.QuizId)); + if (quiz is null) return Errors.Quiz.NotFound; + return quiz; + } +} diff --git a/src/Quizer.Application/Quizes/Queries/GetQuizByName/GetQuizByNameQuery.cs b/src/Quizer.Application/Quizes/Queries/GetQuizByName/GetQuizByNameQuery.cs new file mode 100644 index 0000000..3ca6bbd --- /dev/null +++ b/src/Quizer.Application/Quizes/Queries/GetQuizByName/GetQuizByNameQuery.cs @@ -0,0 +1,9 @@ +using ErrorOr; +using MediatR; +using Quizer.Domain.QuizAggregate; + +namespace Quizer.Application.Quizes.Queries.GetQuizByName; + +public record GetQuizByNameQuery( + string UserName, + string QuizName) : IRequest>; diff --git a/src/Quizer.Application/Quizes/Queries/GetQuizByName/GetQuizByNameQueryHandler.cs b/src/Quizer.Application/Quizes/Queries/GetQuizByName/GetQuizByNameQueryHandler.cs new file mode 100644 index 0000000..0edd858 --- /dev/null +++ b/src/Quizer.Application/Quizes/Queries/GetQuizByName/GetQuizByNameQueryHandler.cs @@ -0,0 +1,26 @@ +using ErrorOr; +using MediatR; +using Microsoft.AspNetCore.Identity; +using Quizer.Application.Common.Interfaces.Persistance; +using Quizer.Domain.Common.Errors; +using Quizer.Domain.QuizAggregate; + +namespace Quizer.Application.Quizes.Queries.GetQuizByName; + +public class GetQuizByNameQueryHandler : IRequestHandler> +{ + private readonly IQuizRepository _quizRepository; + + public GetQuizByNameQueryHandler(IQuizRepository quizRepository) + { + _quizRepository = quizRepository; + } + + public async Task> Handle(GetQuizByNameQuery request, CancellationToken cancellationToken) + { + var quiz = await _quizRepository.Get(request.UserName, request.QuizName); + if (quiz is null) return Errors.Quiz.NotFound; + + return quiz; + } +} diff --git a/src/Quizer.Application/Services/DependencyInjection.cs b/src/Quizer.Application/Services/DependencyInjection.cs new file mode 100644 index 0000000..7fcb66c --- /dev/null +++ b/src/Quizer.Application/Services/DependencyInjection.cs @@ -0,0 +1,17 @@ +using Diacritics; +using Microsoft.Extensions.DependencyInjection; +using Quizer.Application.Services.Slugify; + +namespace Quizer.Application.Services; + +public static class DependencyInjection +{ + public static IServiceCollection AddServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} + diff --git a/src/Quizer.Application/Services/Slugify/ISlugifyService.cs b/src/Quizer.Application/Services/Slugify/ISlugifyService.cs new file mode 100644 index 0000000..6c11c09 --- /dev/null +++ b/src/Quizer.Application/Services/Slugify/ISlugifyService.cs @@ -0,0 +1,7 @@ +namespace Quizer.Application.Services.Slugify +{ + public interface ISlugifyService + { + string GenerateSlug(string str); + } +} \ No newline at end of file diff --git a/src/Quizer.Application/Services/Slugify/SlugifyService.cs b/src/Quizer.Application/Services/Slugify/SlugifyService.cs new file mode 100644 index 0000000..33155f0 --- /dev/null +++ b/src/Quizer.Application/Services/Slugify/SlugifyService.cs @@ -0,0 +1,71 @@ +using System.Text.RegularExpressions; +using System.Text; +using Diacritics; + +namespace Quizer.Application.Services.Slugify +{ + public class SlugifyService : ISlugifyService + { + protected readonly Config _config; + protected readonly IDiacriticsMapper _diacriticsMapper; + + public SlugifyService(IDiacriticsMapper diacriticsMapper) + { + _config = new Config(); + _diacriticsMapper = diacriticsMapper; + } + + public string GenerateSlug(string str) + { + if (_config.ForceLowerCase) + str = str.ToLower(); + + str = CleanWhiteSpace(str, _config.CollapseWhiteSpace); + str = ApplyReplacements(str, _config.CharacterReplacements); + str = _diacriticsMapper.RemoveDiacritics(str); + str = DeleteCharacters(str, _config.DeniedCharactersRegex); + + return str; + } + + protected string CleanWhiteSpace(string str, bool collapse) + { + return Regex.Replace(str, collapse ? @"\s+" : @"\s", " "); + } + + protected string ApplyReplacements(string str, Dictionary replacements) + { + StringBuilder sb = new StringBuilder(str); + + foreach (KeyValuePair replacement in replacements) + sb.Replace(replacement.Key, replacement.Value); + + return sb.ToString(); + } + + protected string DeleteCharacters(string str, string regex) + { + return Regex.Replace(str, regex, ""); + } + + public class Config + { + public Dictionary CharacterReplacements { get; set; } + public bool ForceLowerCase { get; set; } + public bool CollapseWhiteSpace { get; set; } + public string DeniedCharactersRegex { get; set; } + + public Config() + { + CharacterReplacements = new Dictionary + { + { " ", "-" } + }; + + ForceLowerCase = true; + CollapseWhiteSpace = true; + DeniedCharactersRegex = @"[^a-zA-Z0-9\-\._]"; + } + } + } +} diff --git a/src/Quizer.Domain/QuizAggregate/Quiz.cs b/src/Quizer.Domain/QuizAggregate/Quiz.cs index a70d8d3..35b1387 100644 --- a/src/Quizer.Domain/QuizAggregate/Quiz.cs +++ b/src/Quizer.Domain/QuizAggregate/Quiz.cs @@ -5,7 +5,6 @@ using Quizer.Domain.QuizAggregate.Entities; using Quizer.Domain.QuizAggregate.Events; using Quizer.Domain.QuizAggregate.Validation; -using static Quizer.Domain.Common.Errors.Errors; namespace Quizer.Domain.QuizAggregate; @@ -14,23 +13,29 @@ public sealed class Quiz : AggregateRoot private readonly List _questions = new(); public Guid UserId { get; private set; } + public string UserName { get; private set; } public string Name { get; private set; } + public string Slug { get; private set; } public string Description { get; private set; } public AverageRating AverageRating { get; private set; } public IReadOnlyList Questions => _questions.AsReadOnly(); private Quiz( QuizId id, - Guid userId, string name, + string slug, string description, + Guid userId, + string userName, AverageRating averageRating, List questions) : base(id) { Name = name; - UserId = userId; + Slug = slug; Description = description; + UserId = userId; + UserName = userName; AverageRating = averageRating; _questions = questions; } @@ -44,16 +49,20 @@ private ErrorOr Validate() public static ErrorOr Create( string name, + string slug, string description, Guid userId, + string userName, AverageRating averageRating, List questions) { var quiz = new Quiz( QuizId.CreateUnique(), - userId, name, + slug, description, + userId, + userName, averageRating, questions); @@ -67,11 +76,13 @@ public static ErrorOr Create( public ErrorOr Update( string name, + string slug, string description) { base.Update(); Name = name; Description = description; + Slug = slug; var result = this.Validate(); if (result.IsError) return result.Errors; diff --git a/src/Quizer.Infrastructure/Persistance/Configuration/QuizConfigurations.cs b/src/Quizer.Infrastructure/Persistance/Configuration/QuizConfigurations.cs index 8c67e14..79e07b0 100644 --- a/src/Quizer.Infrastructure/Persistance/Configuration/QuizConfigurations.cs +++ b/src/Quizer.Infrastructure/Persistance/Configuration/QuizConfigurations.cs @@ -61,7 +61,7 @@ private static void ConfigureQuizTable(EntityTypeBuilder builder) .HasMaxLength(100) .IsRequired(); - builder.HasIndex(q => q.Name).IsUnique(); + builder.HasIndex(q => new { q.UserName, q.Name }).IsUnique(); builder.Property(q => q.Description) .HasMaxLength(1000) diff --git a/src/Quizer.Infrastructure/Persistance/Repositories/QuizRepository.cs b/src/Quizer.Infrastructure/Persistance/Repositories/QuizRepository.cs index e8173df..1a7e774 100644 --- a/src/Quizer.Infrastructure/Persistance/Repositories/QuizRepository.cs +++ b/src/Quizer.Infrastructure/Persistance/Repositories/QuizRepository.cs @@ -28,9 +28,9 @@ public void Delete(Quiz quiz) return await _context.Quizes.FirstOrDefaultAsync(q => q.Id == id); } - public async Task Get(string name) + public async Task Get(string userName, string quizName) { - return await _context.Quizes.FirstOrDefaultAsync(q => q.Name == name); + return await _context.Quizes.FirstOrDefaultAsync(q => q.UserName == userName && q.Name == quizName); } public async Task> GetAll(Guid? userId = null) From c959731890aec5d7f7c6bb2b6b6a7da2eb2b6d9b Mon Sep 17 00:00:00 2001 From: elotoja Date: Tue, 26 Dec 2023 13:57:18 +0100 Subject: [PATCH 3/3] Fix GetQuizByName --- .../Controllers/V1/QuizController.cs | 1 + src/Quizer.Contracts/Quiz/QuizResponse.cs | 2 + .../20231226124703_FixQuizIndex.Designer.cs | 379 ++++++++++++++++++ .../Migrations/20231226124703_FixQuizIndex.cs | 60 +++ .../QuizerDbContextModelSnapshot.cs | 10 +- 5 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 src/Quizer.Infrastructure/Migrations/20231226124703_FixQuizIndex.Designer.cs create mode 100644 src/Quizer.Infrastructure/Migrations/20231226124703_FixQuizIndex.cs diff --git a/src/Quizer.Api/Controllers/V1/QuizController.cs b/src/Quizer.Api/Controllers/V1/QuizController.cs index 4bcc949..67e47c8 100644 --- a/src/Quizer.Api/Controllers/V1/QuizController.cs +++ b/src/Quizer.Api/Controllers/V1/QuizController.cs @@ -6,6 +6,7 @@ using Quizer.Application.Quizes.Commands.DeleteQuiz; using Quizer.Application.Quizes.Commands.UpdateQuiz; using Quizer.Application.Quizes.Queries.GetQuizById; +using Quizer.Application.Quizes.Queries.GetQuizByName; using Quizer.Application.Quizes.Queries.GetQuizes; using Quizer.Contracts.Quiz; using System.Security.Claims; diff --git a/src/Quizer.Contracts/Quiz/QuizResponse.cs b/src/Quizer.Contracts/Quiz/QuizResponse.cs index 027b384..31d9226 100644 --- a/src/Quizer.Contracts/Quiz/QuizResponse.cs +++ b/src/Quizer.Contracts/Quiz/QuizResponse.cs @@ -3,7 +3,9 @@ public record QuizResponse( string Id, string UserId, + string UserName, string Name, + string Slug, string Description, double AverageRating, int NumberOfRatings, diff --git a/src/Quizer.Infrastructure/Migrations/20231226124703_FixQuizIndex.Designer.cs b/src/Quizer.Infrastructure/Migrations/20231226124703_FixQuizIndex.Designer.cs new file mode 100644 index 0000000..9748319 --- /dev/null +++ b/src/Quizer.Infrastructure/Migrations/20231226124703_FixQuizIndex.Designer.cs @@ -0,0 +1,379 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Quizer.Infrastructure.Persistance; + +#nullable disable + +namespace Quizer.Infrastructure.Migrations +{ + [DbContext(typeof(QuizerDbContext))] + [Migration("20231226124703_FixQuizIndex")] + partial class FixQuizIndex + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Quizer.Domain.QuizAggregate.Quiz", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserName", "Name") + .IsUnique(); + + b.ToTable("Quizes", (string)null); + }); + + modelBuilder.Entity("Quizer.Domain.UserAggregate.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Quizer.Domain.UserAggregate.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Quizer.Domain.UserAggregate.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Quizer.Domain.UserAggregate.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Quizer.Domain.UserAggregate.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Quizer.Domain.QuizAggregate.Quiz", b => + { + b.OwnsOne("Quizer.Domain.Common.ValueObjects.AverageRating", "AverageRating", b1 => + { + b1.Property("QuizId") + .HasColumnType("uuid"); + + b1.Property("NumRatings") + .HasColumnType("integer"); + + b1.Property("Value") + .HasColumnType("double precision"); + + b1.HasKey("QuizId"); + + b1.ToTable("Quizes"); + + b1.WithOwner() + .HasForeignKey("QuizId"); + }); + + b.OwnsMany("Quizer.Domain.QuizAggregate.Entities.Question", "Questions", b1 => + { + b1.Property("Id") + .HasColumnType("uuid") + .HasColumnName("QuestionId"); + + b1.Property("QuizId") + .HasColumnType("uuid"); + + b1.Property("Answer") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b1.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b1.Property("QuestionText") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b1.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b1.HasKey("Id", "QuizId"); + + b1.HasIndex("QuizId"); + + b1.ToTable("Questions", (string)null); + + b1.WithOwner() + .HasForeignKey("QuizId"); + }); + + b.Navigation("AverageRating") + .IsRequired(); + + b.Navigation("Questions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Quizer.Infrastructure/Migrations/20231226124703_FixQuizIndex.cs b/src/Quizer.Infrastructure/Migrations/20231226124703_FixQuizIndex.cs new file mode 100644 index 0000000..2d29d5e --- /dev/null +++ b/src/Quizer.Infrastructure/Migrations/20231226124703_FixQuizIndex.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Quizer.Infrastructure.Migrations +{ + /// + public partial class FixQuizIndex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Quizes_Name", + table: "Quizes"); + + migrationBuilder.AddColumn( + name: "Slug", + table: "Quizes", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "UserName", + table: "Quizes", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: "IX_Quizes_UserName_Name", + table: "Quizes", + columns: new[] { "UserName", "Name" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Quizes_UserName_Name", + table: "Quizes"); + + migrationBuilder.DropColumn( + name: "Slug", + table: "Quizes"); + + migrationBuilder.DropColumn( + name: "UserName", + table: "Quizes"); + + migrationBuilder.CreateIndex( + name: "IX_Quizes_Name", + table: "Quizes", + column: "Name", + unique: true); + } + } +} diff --git a/src/Quizer.Infrastructure/Migrations/QuizerDbContextModelSnapshot.cs b/src/Quizer.Infrastructure/Migrations/QuizerDbContextModelSnapshot.cs index 8bdc955..afa07be 100644 --- a/src/Quizer.Infrastructure/Migrations/QuizerDbContextModelSnapshot.cs +++ b/src/Quizer.Infrastructure/Migrations/QuizerDbContextModelSnapshot.cs @@ -172,15 +172,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(100) .HasColumnType("character varying(100)"); + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.Property("UserId") .HasColumnType("uuid"); + b.Property("UserName") + .IsRequired() + .HasColumnType("text"); + b.HasKey("Id"); - b.HasIndex("Name") + b.HasIndex("UserName", "Name") .IsUnique(); b.ToTable("Quizes", (string)null);