From 33c80120a73df545a9d8ff919858dba808d93f2d Mon Sep 17 00:00:00 2001 From: Alen Alex Date: Tue, 15 Oct 2024 23:47:18 +0530 Subject: [PATCH] Added search, fixed refresh token not saving for google oAuth etc --- .../Dto/Me/SearchDto.cs | 12 ++++++ .../Auth/OAuth/OAuthCommandHandler.cs | 3 +- .../Features/Profile/Search/MeSearchQuery.cs | 10 +++++ .../Profile/Search/MeSearchQueryHandler.cs | 29 +++++++++++++++ .../Profile/Search/MeSearchResponse.cs | 8 ++++ .../Services/IProfileService.cs | 4 +- .../Constants/ServiceType.cs | 10 +++++ .../Models/Files/File.cs | 2 +- MyServe.Backend.App.Domain/Models/IEntity.cs | 6 +++ .../Models/Profile/Profile.cs | 2 +- .../Models/User/User.cs | 2 +- .../Repositories/IProfileRepository.cs | 2 + .../Repositories/ProfileRepository.cs | 37 +++++++++++++++++++ .../Services/ProfileService.cs | 30 +++++++++++++++ .../Controllers/MeController.cs | 13 ++++++- .../Validators/MeValidator.cs | 16 ++++++++ .../Tables/files/files.file.sql | 7 +++- .../Tables/init.sql | 4 +- MyServe.sln | 5 --- 19 files changed, 188 insertions(+), 14 deletions(-) create mode 100644 MyServe.Backend.App.Application/Dto/Me/SearchDto.cs create mode 100644 MyServe.Backend.App.Application/Features/Profile/Search/MeSearchQuery.cs create mode 100644 MyServe.Backend.App.Application/Features/Profile/Search/MeSearchQueryHandler.cs create mode 100644 MyServe.Backend.App.Application/Features/Profile/Search/MeSearchResponse.cs create mode 100644 MyServe.Backend.App.Common/Constants/ServiceType.cs create mode 100644 MyServe.Backend.App.Domain/Models/IEntity.cs create mode 100644 MyServe.Backend.Http.Api/Validators/MeValidator.cs diff --git a/MyServe.Backend.App.Application/Dto/Me/SearchDto.cs b/MyServe.Backend.App.Application/Dto/Me/SearchDto.cs new file mode 100644 index 0000000..4d890e5 --- /dev/null +++ b/MyServe.Backend.App.Application/Dto/Me/SearchDto.cs @@ -0,0 +1,12 @@ +using MyServe.Backend.Common.Constants; + +namespace MyServe.Backend.App.Application.Dto.Me; + +public class SearchDto +{ + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Service { get; set; } + public Dictionary Metadata { get; set; } = new(); +} \ No newline at end of file diff --git a/MyServe.Backend.App.Application/Features/Auth/OAuth/OAuthCommandHandler.cs b/MyServe.Backend.App.Application/Features/Auth/OAuth/OAuthCommandHandler.cs index 065a3ad..302c12a 100644 --- a/MyServe.Backend.App.Application/Features/Auth/OAuth/OAuthCommandHandler.cs +++ b/MyServe.Backend.App.Application/Features/Auth/OAuth/OAuthCommandHandler.cs @@ -53,7 +53,7 @@ public async Task Handle(OAuthCommand request, CancellationToken var refreshTokenTask = refreshTokenService.CreateRefreshTokenAsync(user.Id); await Task.WhenAll(accessTokenTask, refreshTokenTask); - + await uow.CommitAsync(); return new OAuthResponse() { Success = true, @@ -64,6 +64,7 @@ public async Task Handle(OAuthCommand request, CancellationToken catch (Exception e) { logger.Error(e, "An unknown error occured while generating the user access for {EmailAddress}", userIdentificationDto.Email); + await uow.RollbackAsync(); return new OAuthResponse() { Success = false, diff --git a/MyServe.Backend.App.Application/Features/Profile/Search/MeSearchQuery.cs b/MyServe.Backend.App.Application/Features/Profile/Search/MeSearchQuery.cs new file mode 100644 index 0000000..8ee41e3 --- /dev/null +++ b/MyServe.Backend.App.Application/Features/Profile/Search/MeSearchQuery.cs @@ -0,0 +1,10 @@ +using MyServe.Backend.App.Application.Abstract; + +namespace MyServe.Backend.App.Application.Features.Profile.Search; + +public class MeSearchQuery : IAppRequest +{ + public Guid UserId { get; set; } + + public string Search { get; set; } +} \ No newline at end of file diff --git a/MyServe.Backend.App.Application/Features/Profile/Search/MeSearchQueryHandler.cs b/MyServe.Backend.App.Application/Features/Profile/Search/MeSearchQueryHandler.cs new file mode 100644 index 0000000..2f7c5f4 --- /dev/null +++ b/MyServe.Backend.App.Application/Features/Profile/Search/MeSearchQueryHandler.cs @@ -0,0 +1,29 @@ +using MediatR; +using MyServe.Backend.App.Application.Services; +using MyServe.Backend.App.Domain.Abstracts; + +namespace MyServe.Backend.App.Application.Features.Profile.Search; + +public class MeSearchQueryHandler( + IProfileService profileService, + IReadOnlyUnitOfWork readOnlyUnitOfWork + ) : IRequestHandler +{ + public async Task Handle(MeSearchQuery request, CancellationToken cancellationToken) + { + await using var uow = await readOnlyUnitOfWork.StartTransactionAsync(); + try + { + var matchedList = await profileService.SearchAsync(request, cancellationToken); + return new MeSearchResponse() + { + Matched = matchedList + }; + } + catch (Exception e) + { + await uow.RollbackAsync(); + return new MeSearchResponse(); + } + } +} \ No newline at end of file diff --git a/MyServe.Backend.App.Application/Features/Profile/Search/MeSearchResponse.cs b/MyServe.Backend.App.Application/Features/Profile/Search/MeSearchResponse.cs new file mode 100644 index 0000000..4ae7b57 --- /dev/null +++ b/MyServe.Backend.App.Application/Features/Profile/Search/MeSearchResponse.cs @@ -0,0 +1,8 @@ +using MyServe.Backend.App.Application.Dto.Me; + +namespace MyServe.Backend.App.Application.Features.Profile.Search; + +public class MeSearchResponse +{ + public List Matched = []; +} \ No newline at end of file diff --git a/MyServe.Backend.App.Application/Services/IProfileService.cs b/MyServe.Backend.App.Application/Services/IProfileService.cs index 86d9bdc..994bf51 100644 --- a/MyServe.Backend.App.Application/Services/IProfileService.cs +++ b/MyServe.Backend.App.Application/Services/IProfileService.cs @@ -1,11 +1,13 @@ +using MyServe.Backend.App.Application.Dto.Me; using MyServe.Backend.App.Application.Dto.Profile; using MyServe.Backend.App.Application.Features.Profile.Create; +using MyServe.Backend.App.Application.Features.Profile.Search; namespace MyServe.Backend.App.Application.Services; public interface IProfileService { Task GetProfileAsync(Guid userId, CancellationToken cancellationToken = default); - Task CreateNewProfileAsync(CreateProfileCommand command, CancellationToken cancellationToken = default); + Task> SearchAsync(MeSearchQuery searchQuery, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/MyServe.Backend.App.Common/Constants/ServiceType.cs b/MyServe.Backend.App.Common/Constants/ServiceType.cs new file mode 100644 index 0000000..4c1b874 --- /dev/null +++ b/MyServe.Backend.App.Common/Constants/ServiceType.cs @@ -0,0 +1,10 @@ +namespace MyServe.Backend.Common.Constants; + +public enum ServiceType +{ + File, + Password, + Calendar, + Notes, + ShareableLink +} \ No newline at end of file diff --git a/MyServe.Backend.App.Domain/Models/Files/File.cs b/MyServe.Backend.App.Domain/Models/Files/File.cs index 202afc0..731c655 100644 --- a/MyServe.Backend.App.Domain/Models/Files/File.cs +++ b/MyServe.Backend.App.Domain/Models/Files/File.cs @@ -1,6 +1,6 @@ namespace MyServe.Backend.App.Domain.Models.Files; -public class File +public class File : IEntity { public Guid Id { get; set; } public string Name { get; set; } diff --git a/MyServe.Backend.App.Domain/Models/IEntity.cs b/MyServe.Backend.App.Domain/Models/IEntity.cs new file mode 100644 index 0000000..587f5dd --- /dev/null +++ b/MyServe.Backend.App.Domain/Models/IEntity.cs @@ -0,0 +1,6 @@ +namespace MyServe.Backend.App.Domain.Models; + +public interface IEntity +{ + public Guid Id { get; set; } +} \ No newline at end of file diff --git a/MyServe.Backend.App.Domain/Models/Profile/Profile.cs b/MyServe.Backend.App.Domain/Models/Profile/Profile.cs index 784c764..5cce01f 100644 --- a/MyServe.Backend.App.Domain/Models/Profile/Profile.cs +++ b/MyServe.Backend.App.Domain/Models/Profile/Profile.cs @@ -3,7 +3,7 @@ namespace MyServe.Backend.App.Domain.Models.Profile; -public class Profile +public class Profile : IEntity { public Profile() { diff --git a/MyServe.Backend.App.Domain/Models/User/User.cs b/MyServe.Backend.App.Domain/Models/User/User.cs index cf7ad45..176acc7 100644 --- a/MyServe.Backend.App.Domain/Models/User/User.cs +++ b/MyServe.Backend.App.Domain/Models/User/User.cs @@ -1,6 +1,6 @@ namespace MyServe.Backend.App.Domain.Models.User; -public class User +public class User : IEntity { public Guid Id { get; set; } public string EmailAddress { get; set; } diff --git a/MyServe.Backend.App.Domain/Repositories/IProfileRepository.cs b/MyServe.Backend.App.Domain/Repositories/IProfileRepository.cs index 6bb4b74..fa5f401 100644 --- a/MyServe.Backend.App.Domain/Repositories/IProfileRepository.cs +++ b/MyServe.Backend.App.Domain/Repositories/IProfileRepository.cs @@ -1,4 +1,5 @@ using MyServe.Backend.App.Domain.Abstracts; +using MyServe.Backend.App.Domain.Models; using MyServe.Backend.App.Domain.Models.Profile; namespace MyServe.Backend.App.Domain.Repositories; @@ -6,4 +7,5 @@ namespace MyServe.Backend.App.Domain.Repositories; public interface IProfileRepository : IAppRepository { Task ExistsAsync(string emailAddress); + Task> SearchAcrossProfileAsync(string search, Guid userId); } \ No newline at end of file diff --git a/MyServe.Backend.App.Infrastructure/Repositories/ProfileRepository.cs b/MyServe.Backend.App.Infrastructure/Repositories/ProfileRepository.cs index f824dbd..4a0dd95 100644 --- a/MyServe.Backend.App.Infrastructure/Repositories/ProfileRepository.cs +++ b/MyServe.Backend.App.Infrastructure/Repositories/ProfileRepository.cs @@ -1,12 +1,15 @@ using Dapper; using Microsoft.AspNetCore.JsonPatch; using Microsoft.Extensions.DependencyInjection; +using MyServe.Backend.App.Domain.Extensions; +using MyServe.Backend.App.Domain.Models; using MyServe.Backend.App.Domain.Models.Profile; using MyServe.Backend.App.Domain.Repositories; using MyServe.Backend.App.Infrastructure.Abstract; using MyServe.Backend.App.Infrastructure.Database.NpgSql; using Newtonsoft.Json; using Npgsql; +using File = MyServe.Backend.App.Domain.Models.Files.File; namespace MyServe.Backend.App.Infrastructure.Repositories; @@ -70,6 +73,30 @@ public async Task ExistsAsync(string emailAddress) return emailList.Any(); } + public async Task> SearchAcrossProfileAsync(string search, Guid userId) + { + List entities = []; + var searchParam = search.Split(" ").Select(x => $"%{x}%").ToList(); + var gridReader = await readOnlyConnection.QueryMultipleAsync(ProfileSql.SearchEntities, new + { + UserId = userId, + @Search = searchParam, + }); + + entities.AddRange(from file in await gridReader.ReadAsync() + let fileTypeRaw = file.type.ToString() + select new File() + { + Id = file.id, + Name = file.name, + ParentId = file.parent, + MimeType = file.mime_type, + TargetSize = file.target_size, + Type = ((string)fileTypeRaw).GetFileTypeFromString()!.Value + }); + return entities; + } + private static class ProfileSql { public const string SelectById = """ @@ -80,5 +107,15 @@ private static class ProfileSql INSERT INTO public.profile (id, first_name, last_name, "profile_settings", "created_at", "profile_image", "encryption_key") VALUES (@Id, @FirstName, @LastName, @Settings, @CreatedAt, @ProfileImage, @EncryptionKey);; """; public const string Exists = "SELECT 1 FROM profile WHERE id = @Email LIMIT 1"; + + public const string SearchEntities = """ + + SELECT id AS "id", "name" AS name, parent AS "parent", "mime_type", "type", target_size + FROM files.file f WHERE f.owner = @UserId AND is_deleted = false AND ( + "name" ILIKE ANY(@Search) OR mime_type ILIKE ANY(@Search) + ) + ORDER BY f.name, f.created + + """; } } \ No newline at end of file diff --git a/MyServe.Backend.App.Infrastructure/Services/ProfileService.cs b/MyServe.Backend.App.Infrastructure/Services/ProfileService.cs index 9024a6f..77bb631 100644 --- a/MyServe.Backend.App.Infrastructure/Services/ProfileService.cs +++ b/MyServe.Backend.App.Infrastructure/Services/ProfileService.cs @@ -1,8 +1,10 @@ using System.Net.Mime; using Microsoft.Extensions.DependencyInjection; using MyServe.Backend.App.Application.Client; +using MyServe.Backend.App.Application.Dto.Me; using MyServe.Backend.App.Application.Dto.Profile; using MyServe.Backend.App.Application.Features.Profile.Create; +using MyServe.Backend.App.Application.Features.Profile.Search; using MyServe.Backend.App.Application.Services; using MyServe.Backend.App.Domain.Models.Profile; using MyServe.Backend.App.Domain.Repositories; @@ -11,6 +13,7 @@ using MyServe.Backend.Common.Constants; using MyServe.Backend.Common.Constants.StorageConstants; using MyServe.Backend.Common.Models; +using File = MyServe.Backend.App.Domain.Models.Files.File; namespace MyServe.Backend.App.Infrastructure.Services; @@ -64,4 +67,31 @@ public async Task CreateNewProfileAsync(CreateProfileCommand command return ProfileMapper.ToProfileDto(profile);; } + + public async Task> SearchAsync(MeSearchQuery searchQuery, CancellationToken cancellationToken = default) + { + var entities = await profileRepository.SearchAcrossProfileAsync(searchQuery.Search, searchQuery.UserId); + List responses = []; + foreach (var entity in entities) + { + if (entity is File file) + { + responses.Add(new SearchDto() + { + Service = ServiceType.File.ToString(), + Name = file.Name, + Id = file.Id, + Description = "", + Metadata = + { + {"ParentId", file.ParentId?.ToString() }, + {"Type", file.Type.ToString()}, + {"MimeType", file.MimeType}, + {"TargetSize", file.TargetSize.ToString()} + } + }); + } + } + return responses; + } } \ No newline at end of file diff --git a/MyServe.Backend.Http.Api/Controllers/MeController.cs b/MyServe.Backend.Http.Api/Controllers/MeController.cs index 88d8f50..882cd09 100644 --- a/MyServe.Backend.Http.Api/Controllers/MeController.cs +++ b/MyServe.Backend.Http.Api/Controllers/MeController.cs @@ -6,6 +6,7 @@ using MyServe.Backend.App.Application.Client; using MyServe.Backend.App.Application.Features.Profile.Create; using MyServe.Backend.App.Application.Features.Profile.Me; +using MyServe.Backend.App.Application.Features.Profile.Search; using Newtonsoft.Json; using ILogger = Serilog.ILogger; @@ -18,7 +19,7 @@ public class MeController(ICacheService cacheService, ILogger logger, IRequestCo [HttpGet] [Authorize] - public async Task Get() + public async Task> Get() { var requesterUserId = requestContext.Requester.UserId; requestContext.CacheControl.FrameEndpointCacheKey(CacheConstants.UserCacheKey, requesterUserId.ToString()); @@ -38,7 +39,7 @@ public async Task Get() [HttpPost] [Authorize] - public async Task Post() + public async Task> Post() { var formCollection = await Request.ReadFormAsync(); if (!formCollection.ContainsKey("body")) @@ -65,5 +66,13 @@ public async Task Post() return CreatedAtRoute("GetUser", new { controller = "User", id = createProfileResponse.Id }, null); } + + [HttpGet("search")] + public async Task> Search([FromQuery] MeSearchQuery searchQuery) + { + searchQuery.UserId = requestContext.Requester.UserId; + var response = await mediator.Send(searchQuery); + return Ok(response); + } } \ No newline at end of file diff --git a/MyServe.Backend.Http.Api/Validators/MeValidator.cs b/MyServe.Backend.Http.Api/Validators/MeValidator.cs new file mode 100644 index 0000000..6f51b95 --- /dev/null +++ b/MyServe.Backend.Http.Api/Validators/MeValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using MyServe.Backend.App.Application.Features.Profile.Search; + +namespace MyServe.Backend.Api.Validators; + +public class MeSearchQueryValidator : AbstractValidator +{ + + public MeSearchQueryValidator() + { + RuleFor(x => x.Search) + .MinimumLength(3) + .WithMessage("Search length must be between 3"); + } + +} \ No newline at end of file diff --git a/MyServe.Backend.Infrastructure.Database/Tables/files/files.file.sql b/MyServe.Backend.Infrastructure.Database/Tables/files/files.file.sql index bf2918d..b77d1a6 100644 --- a/MyServe.Backend.Infrastructure.Database/Tables/files/files.file.sql +++ b/MyServe.Backend.Infrastructure.Database/Tables/files/files.file.sql @@ -54,4 +54,9 @@ $$ LANGUAGE plpgsql; CREATE OR REPLACE TRIGGER validate_parent_type BEFORE INSERT OR UPDATE ON files.file - FOR EACH ROW EXECUTE FUNCTION check_file_validation(); \ No newline at end of file + FOR EACH ROW EXECUTE FUNCTION check_file_validation(); + +CREATE INDEX idx_name_mime_type ON files.file USING gin ( + name gin_trgm_ops, + mime_type gin_trgm_ops + ); \ No newline at end of file diff --git a/MyServe.Backend.Infrastructure.Database/Tables/init.sql b/MyServe.Backend.Infrastructure.Database/Tables/init.sql index b99ece8..76eec25 100644 --- a/MyServe.Backend.Infrastructure.Database/Tables/init.sql +++ b/MyServe.Backend.Infrastructure.Database/Tables/init.sql @@ -1,2 +1,4 @@ CREATE SCHEMA IF NOT EXISTS "public"; -CREATE SCHEMA IF NOT EXISTS "files"; \ No newline at end of file +CREATE SCHEMA IF NOT EXISTS "files"; + +CREATE EXTENSION IF NOT EXISTS pg_trgm; diff --git a/MyServe.sln b/MyServe.sln index 0727e46..6af78fc 100644 --- a/MyServe.sln +++ b/MyServe.sln @@ -3,11 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyServe.Backend.Http.Api", "MyServe.Backend.Http.Api\MyServe.Backend.Http.Api.csproj", "{5013C947-CDE3-4CF6-8767-330481288FAC}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MyServe.Backend.Infrastructure", "MyServe.Backend.Infrastructure", "{619C9F09-AF0F-4A47-8DCC-FB93D6CEFA3C}" - ProjectSection(SolutionItems) = preProject - docker-compose.yml = docker-compose.yml - Dockerfile-Consumer-Job = Dockerfile-Consumer-Job - .dockerignore = .dockerignore - EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyServe.Backend.App.Common", "MyServe.Backend.App.Common\MyServe.Backend.App.Common.csproj", "{2E4F2FF1-2C96-4D1F-937C-9B2634915C40}" EndProject