diff --git a/.vscode/settings.json b/.vscode/settings.json index d9821aa..9cd2af5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "files.exclude": { "**/bin": true - } + }, + "editor.formatOnType": true } \ No newline at end of file diff --git a/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs b/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs index 9c195fb..f3ff806 100644 --- a/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs +++ b/ShoppingAssistantApi.Api/Mutations/WishlistsMutation.cs @@ -7,11 +7,19 @@ namespace ShoppingAssistantApi.Api.Mutations; [ExtendObjectType(OperationTypeNames.Mutation)] public class WishlistsMutation { - public Task StartPersonalWishlist(WishlistCreateDto dto, CancellationToken cancellationToken, - [Service] IWishlistsService wishlistsService) + public Task StartPersonalWishlistAsync(WishlistCreateDto dto, CancellationToken cancellationToken, + [Service] IWishlistsService wishlistsService) => wishlistsService.StartPersonalWishlistAsync(dto, cancellationToken); - public Task AddMessageToPersonalWishlist(string wishlistId, MessageCreateDto dto, CancellationToken cancellationToken, - [Service] IWishlistsService wishlistsService) + public Task AddMessageToPersonalWishlistAsync(string wishlistId, MessageCreateDto dto, CancellationToken cancellationToken, + [Service] IWishlistsService wishlistsService) => wishlistsService.AddMessageToPersonalWishlistAsync(wishlistId, dto, cancellationToken); + + public Task AddProductToPersonalWishlistAsync(string wishlistId, ProductCreateDto dto, CancellationToken cancellationToken, + [Service] IWishlistsService wishlistsService) + => wishlistsService.AddProductToPersonalWishlistAsync(wishlistId, dto, cancellationToken); + + public Task DeletePersonalWishlistAsync(string wishlistId, CancellationToken cancellationToken, + [Service] IWishlistsService wishlistsService) + => wishlistsService.DeletePersonalWishlistAsync(wishlistId, cancellationToken); } diff --git a/ShoppingAssistantApi.Api/Queries/WishlistsQuery.cs b/ShoppingAssistantApi.Api/Queries/WishlistsQuery.cs index 389864f..d3d36be 100644 --- a/ShoppingAssistantApi.Api/Queries/WishlistsQuery.cs +++ b/ShoppingAssistantApi.Api/Queries/WishlistsQuery.cs @@ -9,12 +9,22 @@ namespace ShoppingAssistantApi.Api.Queries; public class WishlistsQuery { [Authorize] - public Task> GetPersonalWishlistsPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken, - [Service] IWishlistsService wishlistsService) - => wishlistsService.GetPersonalWishlistsPageAsync(pageNumber, pageSize, cancellationToken); + public Task> GetPersonalWishlistsPageAsync(int pageNumber, int pageSize, + CancellationToken cancellationToken, [Service] IWishlistsService wishlistsService) + => wishlistsService.GetPersonalWishlistsPageAsync(pageNumber, pageSize, cancellationToken); [Authorize] public Task GetPersonalWishlistAsync(string wishlistId, CancellationToken cancellationToken, - [Service] IWishlistsService wishlistsService) - => wishlistsService.GetPersonalWishlistAsync(wishlistId, cancellationToken); + [Service] IWishlistsService wishlistsService) + => wishlistsService.GetPersonalWishlistAsync(wishlistId, cancellationToken); + + [Authorize] + public Task> GetMessagesPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, + CancellationToken cancellationToken, [Service] IWishlistsService wishlistsService) + => wishlistsService.GetMessagesPageFromPersonalWishlistAsync(wishlistId, pageNumber, pageSize, cancellationToken); + + [Authorize] + public Task> GetProductsPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, + CancellationToken cancellationToken, [Service] IWishlistsService wishlistsService) + => wishlistsService.GetProductsPageFromPersonalWishlistAsync(wishlistId, pageNumber, pageSize, cancellationToken); } diff --git a/ShoppingAssistantApi.Application/IRepositories/IMessagerepository.cs b/ShoppingAssistantApi.Application/IRepositories/IMessagerepository.cs index 5387549..3d0483e 100644 --- a/ShoppingAssistantApi.Application/IRepositories/IMessagerepository.cs +++ b/ShoppingAssistantApi.Application/IRepositories/IMessagerepository.cs @@ -1,5 +1,9 @@ +using System.Linq.Expressions; using ShoppingAssistantApi.Domain.Entities; namespace ShoppingAssistantApi.Application.IRepositories; -public interface IMessagesRepository : IBaseRepository { } +public interface IMessagesRepository : IBaseRepository +{ + Task> GetPageStartingFromEndAsync(int pageNumber, int pageSize, Expression> predicate, CancellationToken cancellationToken); +} diff --git a/ShoppingAssistantApi.Application/IRepositories/IProductsRepository.cs b/ShoppingAssistantApi.Application/IRepositories/IProductsRepository.cs new file mode 100644 index 0000000..d5c6c02 --- /dev/null +++ b/ShoppingAssistantApi.Application/IRepositories/IProductsRepository.cs @@ -0,0 +1,5 @@ +using ShoppingAssistantApi.Domain.Entities; + +namespace ShoppingAssistantApi.Application.IRepositories; + +public interface IProductsRepository : IBaseRepository { } diff --git a/ShoppingAssistantApi.Application/IServices/IOpenAiService.cs b/ShoppingAssistantApi.Application/IServices/IOpenAiService.cs new file mode 100644 index 0000000..df56a10 --- /dev/null +++ b/ShoppingAssistantApi.Application/IServices/IOpenAiService.cs @@ -0,0 +1,13 @@ +using ShoppingAssistantApi.Application.Models.OpenAi; + +namespace ShoppingAssistantApi.Application.IServices; + +public interface IOpenAiService +{ + Task GetChatCompletion(ChatCompletionRequest chat, CancellationToken cancellationToken); + + /// + /// Retrieves a stream of tokens (pieces of words) based on provided chat. + /// + IAsyncEnumerable GetChatCompletionStream(ChatCompletionRequest chat, CancellationToken cancellationToken); +} diff --git a/ShoppingAssistantApi.Application/IServices/IWishlistService.cs b/ShoppingAssistantApi.Application/IServices/IWishlistService.cs index 5bfae6a..d95a874 100644 --- a/ShoppingAssistantApi.Application/IServices/IWishlistService.cs +++ b/ShoppingAssistantApi.Application/IServices/IWishlistService.cs @@ -13,4 +13,12 @@ public interface IWishlistsService Task> GetPersonalWishlistsPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken); Task GetPersonalWishlistAsync(string wishlistId, CancellationToken cancellationToken); + + Task> GetMessagesPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, CancellationToken cancellationToken); + + Task AddProductToPersonalWishlistAsync(string wishlistId, ProductCreateDto dto, CancellationToken cancellationToken); + + Task> GetProductsPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, CancellationToken cancellationToken); + + Task DeletePersonalWishlistAsync(string wishlistId, CancellationToken cancellationToken); } diff --git a/ShoppingAssistantApi.Application/MappingProfiles/ProductProfile.cs b/ShoppingAssistantApi.Application/MappingProfiles/ProductProfile.cs new file mode 100644 index 0000000..92ab66f --- /dev/null +++ b/ShoppingAssistantApi.Application/MappingProfiles/ProductProfile.cs @@ -0,0 +1,15 @@ +using AutoMapper; +using ShoppingAssistantApi.Application.Models.CreateDtos; +using ShoppingAssistantApi.Application.Models.Dtos; +using ShoppingAssistantApi.Domain.Entities; + +namespace ShoppingAssistantApi.Application.MappingProfiles; +public class ProductProfile : Profile +{ + public ProductProfile() + { + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + } +} diff --git a/ShoppingAssistantApi.Application/Models/CreateDtos/ProductCreateDto.cs b/ShoppingAssistantApi.Application/Models/CreateDtos/ProductCreateDto.cs new file mode 100644 index 0000000..015706c --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/CreateDtos/ProductCreateDto.cs @@ -0,0 +1,16 @@ +namespace ShoppingAssistantApi.Application.Models.CreateDtos; + +public class ProductCreateDto +{ + public required string Url { get; set; } + + public required string Name { get; set; } + + public required string Description { get; set; } + + public required double Rating { get; set; } + + public required string[] ImagesUrls { get; set; } + + public required bool WasOpened { get; set; } +} diff --git a/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs b/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs index a33c92e..9225d00 100644 --- a/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs +++ b/ShoppingAssistantApi.Application/Models/Dtos/MessageDto.cs @@ -8,5 +8,5 @@ public class MessageDto public required string Role { get; set; } - public string? CreatedById { get; set; } = null; + public required string CreatedById { get; set; } } diff --git a/ShoppingAssistantApi.Application/Models/Dtos/ProductDto.cs b/ShoppingAssistantApi.Application/Models/Dtos/ProductDto.cs new file mode 100644 index 0000000..1697cd6 --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/Dtos/ProductDto.cs @@ -0,0 +1,20 @@ +namespace ShoppingAssistantApi.Application.Models.Dtos; + +public class ProductDto +{ + public required string Id { get; set; } + + public required string Url { get; set; } + + public required string Name { get; set; } + + public required string Description { get; set; } + + public required double Rating { get; set; } + + public required string[] ImagesUrls { get; set; } + + public required bool WasOpened { get; set; } + + public required string WishlistId { get; set; } +} diff --git a/ShoppingAssistantApi.Application/Models/Dtos/WishlistDto.cs b/ShoppingAssistantApi.Application/Models/Dtos/WishlistDto.cs index 9398c26..cbb2cf6 100644 --- a/ShoppingAssistantApi.Application/Models/Dtos/WishlistDto.cs +++ b/ShoppingAssistantApi.Application/Models/Dtos/WishlistDto.cs @@ -8,5 +8,5 @@ public class WishlistDto public required string Type { get; set; } - public string CreatedById { get; set; } = null!; + public required string CreatedById { get; set; } } diff --git a/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs b/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs new file mode 100644 index 0000000..d2ad66b --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/OpenAi/ChatCompletionRequest.cs @@ -0,0 +1,14 @@ +namespace ShoppingAssistantApi.Application.Models.OpenAi; + +public class ChatCompletionRequest +{ + public string Model { get; set; } = "gpt-3.5-turbo"; + + public List Messages { get; set; } + + public double Temperature { get; set; } = 0.7; + + public int MaxTokens { get; set; } = 256; + + public bool Stream { get; set; } = false; +} diff --git a/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiMessage.cs b/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiMessage.cs new file mode 100644 index 0000000..91bd757 --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiMessage.cs @@ -0,0 +1,10 @@ +using ShoppingAssistantApi.Domain.Enums; + +namespace ShoppingAssistantApi.Application.Models.OpenAi; + +public class OpenAiMessage +{ + public OpenAiRole Role { get; set; } + + public string Content { get; set; } +} diff --git a/ShoppingAssistantApi.Domain/Entities/Message.cs b/ShoppingAssistantApi.Domain/Entities/Message.cs index 8a50457..1d9328e 100644 --- a/ShoppingAssistantApi.Domain/Entities/Message.cs +++ b/ShoppingAssistantApi.Domain/Entities/Message.cs @@ -5,9 +5,9 @@ namespace ShoppingAssistantApi.Domain.Entities; public class Message : EntityBase { - public required string Text { get; set; } + public string Text { get; set; } - public required string Role { get; set; } + public string Role { get; set; } public ObjectId WishlistId { get; set; } } diff --git a/ShoppingAssistantApi.Domain/Entities/Product.cs b/ShoppingAssistantApi.Domain/Entities/Product.cs new file mode 100644 index 0000000..2085293 --- /dev/null +++ b/ShoppingAssistantApi.Domain/Entities/Product.cs @@ -0,0 +1,22 @@ +using MongoDB.Bson; +using ShoppingAssistantApi.Domain.Common; + +namespace ShoppingAssistantApi.Domain.Entities; + +public class Product : EntityBase +{ + + public string Url { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public double Rating { get; set; } + + public string[] ImagesUrls { get; set; } + + public bool WasOpened { get; set; } + + public ObjectId WishlistId { get; set; } +} diff --git a/ShoppingAssistantApi.Domain/Entities/Wishlist.cs b/ShoppingAssistantApi.Domain/Entities/Wishlist.cs index 11fe978..4dee7fc 100644 --- a/ShoppingAssistantApi.Domain/Entities/Wishlist.cs +++ b/ShoppingAssistantApi.Domain/Entities/Wishlist.cs @@ -4,9 +4,9 @@ namespace ShoppingAssistantApi.Domain.Entities; public class Wishlist : EntityBase { - public required string Name { get; set; } + public string Name { get; set; } - public required string Type { get; set; } + public string Type { get; set; } public ICollection? Messages { get; set; } } diff --git a/ShoppingAssistantApi.Domain/Enums/OpenAiRole.cs b/ShoppingAssistantApi.Domain/Enums/OpenAiRole.cs new file mode 100644 index 0000000..54d2c0a --- /dev/null +++ b/ShoppingAssistantApi.Domain/Enums/OpenAiRole.cs @@ -0,0 +1,8 @@ +namespace ShoppingAssistantApi.Domain.Enums; + +public enum OpenAiRole +{ + System, + User, + Assistant +} diff --git a/ShoppingAssistantApi.Infrastructure/Services/OpenAiService.cs b/ShoppingAssistantApi.Infrastructure/Services/OpenAiService.cs new file mode 100644 index 0000000..9ed750a --- /dev/null +++ b/ShoppingAssistantApi.Infrastructure/Services/OpenAiService.cs @@ -0,0 +1,24 @@ +using ShoppingAssistantApi.Application.IServices; +using ShoppingAssistantApi.Application.Models.OpenAi; + +namespace ShoppingAssistantApi.Infrastructure.Services; + +public class OpenAiService : IOpenAiService +{ + private readonly HttpClient _httpClient; + + public OpenAiService(HttpClient client) + { + _httpClient = client; + } + + public Task GetChatCompletion(ChatCompletionRequest chat, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable GetChatCompletionStream(ChatCompletionRequest chat, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} diff --git a/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs b/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs index 7e1fc40..5dd2ae7 100644 --- a/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs +++ b/ShoppingAssistantApi.Infrastructure/Services/WishlistsService.cs @@ -18,12 +18,15 @@ public class WishlistsService : IWishlistsService private readonly IMessagesRepository _messagesRepository; + private readonly IProductsRepository _productsRepository; + private readonly IMapper _mapper; - public WishlistsService(IWishlistsRepository wishlistRepository, IMessagesRepository messageRepository, IMapper mapper) + public WishlistsService(IWishlistsRepository wishlistRepository, IMessagesRepository messageRepository, IProductsRepository productRepository, IMapper mapper) { _wishlistsRepository = wishlistRepository; _messagesRepository = messageRepository; + _productsRepository = productRepository; _mapper = mapper; } @@ -47,6 +50,8 @@ public class WishlistsService : IWishlistsService { Text = dto.FirstMessageText, Role = MessageRoles.User.ToString(), + CreatedById = (ObjectId) GlobalUser.Id, + CreatedDateUtc = DateTime.UtcNow, WishlistId = createdWishlist.Id }; var createdMessage = await _messagesRepository.AddAsync(newMessage, cancellationToken); @@ -62,17 +67,13 @@ public class WishlistsService : IWishlistsService { throw new InvalidDataException("Provided id is invalid."); } - newMessage.WishlistId = wishlistObjectId; + newMessage.Role = MessageRoles.User.ToString(); newMessage.CreatedById = (ObjectId) GlobalUser.Id; newMessage.CreatedDateUtc = DateTime.UtcNow; + newMessage.WishlistId = wishlistObjectId; - var relatedWishlist = await _wishlistsRepository.GetWishlistAsync(x => x.Id == wishlistObjectId && x.CreatedById == GlobalUser.Id, cancellationToken); - - if (relatedWishlist == null) - { - throw new UnAuthorizedException(); - } + await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); var createdMessage = await _messagesRepository.AddAsync(newMessage, cancellationToken); @@ -81,7 +82,7 @@ public class WishlistsService : IWishlistsService public async Task> GetPersonalWishlistsPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken) { - var entities = await _wishlistsRepository.GetPageAsync(pageNumber, pageSize, cancellationToken); + var entities = await _wishlistsRepository.GetPageAsync(pageNumber, pageSize, x => x.CreatedById == GlobalUser.Id, cancellationToken); var dtos = _mapper.Map>(entities); var count = await _wishlistsRepository.GetTotalCountAsync(); return new PagedList(dtos, pageNumber, pageSize, count); @@ -93,15 +94,95 @@ public class WishlistsService : IWishlistsService { throw new InvalidDataException("Provided id is invalid."); } - var entity = await _wishlistsRepository.GetWishlistAsync(x => x.Id == wishlistObjectId && x.CreatedById == GlobalUser.Id, cancellationToken); - Console.WriteLine(" WISHLIST: " + entity.CreatedById + " " + GlobalUser.Id); - - if (entity == null) - { - throw new UnAuthorizedException(); - } + var entity = await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); return _mapper.Map(entity); } + + public async Task> GetMessagesPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, CancellationToken cancellationToken) + { + if (!ObjectId.TryParse(wishlistId, out var wishlistObjectId)) + { + throw new InvalidDataException("Provided id is invalid."); + } + + await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); + + var entities = await _messagesRepository.GetPageStartingFromEndAsync(pageNumber, pageSize, x => x.WishlistId == wishlistObjectId, cancellationToken); + + var dtos = _mapper.Map>(entities); + var count = await _messagesRepository.GetCountAsync(x => x.WishlistId == wishlistObjectId, cancellationToken); + return new PagedList(dtos, pageNumber, pageSize, count); + } + + public async Task AddProductToPersonalWishlistAsync(string wishlistId, ProductCreateDto dto, CancellationToken cancellationToken) + { + var newProduct = _mapper.Map(dto); + + if (!ObjectId.TryParse(wishlistId, out var wishlistObjectId)) + { + throw new InvalidDataException("Provided id is invalid."); + } + + await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); + + newProduct.CreatedById = (ObjectId) GlobalUser.Id; + newProduct.CreatedDateUtc = DateTime.UtcNow; + newProduct.WishlistId = wishlistObjectId; + + var createdProduct = await _productsRepository.AddAsync(newProduct, cancellationToken); + + return _mapper.Map(createdProduct); + } + + public async Task> GetProductsPageFromPersonalWishlistAsync(string wishlistId, int pageNumber, int pageSize, CancellationToken cancellationToken) + { + if (!ObjectId.TryParse(wishlistId, out var wishlistObjectId)) + { + throw new InvalidDataException("Provided id is invalid."); + } + + await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); + + var entities = await _productsRepository.GetPageAsync(pageNumber, pageSize, x => x.WishlistId == wishlistObjectId, cancellationToken); + + var dtos = _mapper.Map>(entities); + var count = await _productsRepository.GetCountAsync(x => x.WishlistId == wishlistObjectId, cancellationToken); + return new PagedList(dtos, pageNumber, pageSize, count); + } + + public async Task DeletePersonalWishlistAsync(string wishlistId, CancellationToken cancellationToken) + { + if (!ObjectId.TryParse(wishlistId, out var wishlistObjectId)) + { + throw new InvalidDataException("Provided id is invalid."); + } + + var entity = await TryGetPersonalWishlist(wishlistObjectId, cancellationToken); + + entity.LastModifiedById = GlobalUser.Id; + entity.LastModifiedDateUtc = DateTime.UtcNow; + + await _wishlistsRepository.DeleteAsync(entity, cancellationToken); + + return _mapper.Map(entity); + } + + private async Task TryGetPersonalWishlist(ObjectId wishlistId, CancellationToken cancellationToken) + { + var entity = await _wishlistsRepository.GetWishlistAsync(x => x.Id == wishlistId, cancellationToken); + + if (entity.CreatedById != GlobalUser.Id) + { + throw new UnAuthorizedException(); + } + + if (entity == null) + { + throw new EntityNotFoundException(); + } + + return entity; + } } diff --git a/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs b/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs index a63c8f7..f96d491 100644 --- a/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs +++ b/ShoppingAssistantApi.Persistance/PersistanceExtentions/DbInitialaizer.cs @@ -23,9 +23,13 @@ public class DbInitialaizer private readonly ITokensService _tokensService; + private readonly IWishlistsService _wishlistsService; + private readonly IMongoCollection _userCollection; private readonly IMongoCollection _wishlistCollection; + + private readonly IMongoCollection _productCollection; public IEnumerable Roles { get; set; } @@ -35,8 +39,10 @@ public class DbInitialaizer _rolesService = serviceProvider.GetService(); _userManager = serviceProvider.GetService(); _tokensService = serviceProvider.GetService(); + _wishlistsService = serviceProvider.GetService(); _wishlistCollection = serviceProvider.GetService().Db.GetCollection("Wishlists"); _userCollection = serviceProvider.GetService().Db.GetCollection("Users"); + _productCollection = serviceProvider.GetService().Db.GetCollection("Product"); } public async Task InitialaizeDb(CancellationToken cancellationToken) @@ -44,6 +50,7 @@ public class DbInitialaizer await AddRoles(cancellationToken); await AddUsers(cancellationToken); await AddWishlistsWithMessages(cancellationToken); + await AddProducts(cancellationToken); } public async Task AddUsers(CancellationToken cancellationToken) @@ -186,11 +193,17 @@ public class DbInitialaizer { Text = "Prompt", Role = MessageRoles.User.ToString(), + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd"), + CreatedById = user1.Id, + CreatedDateUtc = DateTime.UtcNow }, new Message { Text = "Answer", Role = MessageRoles.Application.ToString(), + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd"), + CreatedById = user1.Id, + CreatedDateUtc = DateTime.UtcNow }, } }, @@ -206,6 +219,9 @@ public class DbInitialaizer { Text = "Prompt", Role = MessageRoles.User.ToString(), + WishlistId = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), + CreatedById = user1.Id, + CreatedDateUtc = DateTime.UtcNow } } } @@ -213,4 +229,83 @@ public class DbInitialaizer await _wishlistCollection.InsertManyAsync(wishlists); } + + public async Task AddProducts(CancellationToken cancellationToken) + { + var products = new Product[] + { + new Product() + { + Name = "Thermaltake Glacier 360 Liquid-Cooled PC", + Description = "Cool PC for any task!", + Rating = 4.3, + Url = "https://www.amazon.com/Thermaltake-Liquid-Cooled-ToughRAM-Computer-S3WT-B550-G36-LCS/dp" + + "/B09FYNM2GW/ref=sr_1_1?crid=391KAS4JFJSFF&keywords=gaming%2Bpc&qid=1697132083&sprefix=gaming%2Bpc%2Caps%2C209&sr=8-1&th=1", + ImagesUrls = new string[] + { + "https://m.media-amazon.com/images/I/61cXu9yGldL._AC_SL1200_.jpg", + "https://m.media-amazon.com/images/I/615gxSGp42L._AC_SL1200_.jpg" + }, + CreatedDateUtc = DateTime.UtcNow, + WasOpened = false, + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd") + }, + + new Product() + { + Name = "Apple MagSafe Battery Pack", + Description = "Portable Charger with Fast Charging Capability, Power Bank Compatible with iPhone", + Rating = 4.3, + Url = "https://www.amazon.com/Apple-MJWY3AM-A-MagSafe-Battery/dp/" + + "B099BWY7WT/ref=sr_1_1?keywords=apple+power+bank&qid=1697375350&sr=8-1", + ImagesUrls = new string[] + { + "https://m.media-amazon.com/images/I/418SjFMB1wL._AC_SX679_.jpg", + "https://m.media-amazon.com/images/I/51v4pgChtLL._AC_SX679_.jpg", + "https://m.media-amazon.com/images/I/61mJ0z7uYQL._AC_SX679_.jpg" + }, + CreatedDateUtc = DateTime.UtcNow, + WasOpened = false, + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd") + }, + + new Product() + { + Name = "Logitech K400 Plus Wireless Touch With Easy Media Control and Built-in Touchpad", + Description = "Reliable membrane keyboard with touchpad!", + Rating = 4.5, + Url = "https://www.amazon.com/Logitech-Wireless-Keyboard-Touchpad-PC-connected/dp/B014EUQOGK/" + + "ref=sr_1_11?crid=BU2PHZKHKD65&keywords=keyboard+wireless&qid=1697375559&sprefix=keyboard+wir%2Caps%2C195&sr=8-11", + ImagesUrls = new string[] + { + "https://m.media-amazon.com/images/I/51yjnWJ5urL._AC_SX466_.jpg", + "https://m.media-amazon.com/images/I/71al70zP7QL._AC_SX466_.jpg", + "https://m.media-amazon.com/images/I/71+JXDDY01L._AC_SX466_.jpg" + }, + CreatedDateUtc = DateTime.UtcNow, + WasOpened = false, + WishlistId = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab") + }, + + new Product() + { + Name = "Logitech MX Anywhere 2S Wireless Mouse Use On Any Surface", + Description = "Cross computer control: Game changing capacity to navigate seamlessly on three computers," + + " and copy paste text, images, and files from one to the other using Logitech Flow", + Rating = 4.6, + Url = "https://www.amazon.com/Logitech-Hyper-Fast-Scrolling-Rechargeable-Computers/dp/B08P2JFPQC/ref=sr_1_8?" + + "crid=2BL6Z14W2TPP3&keywords=mouse%2Bwireless&qid=1697375784&sprefix=mousewireless%2Caps%2C197&sr=8-8&th=1", + ImagesUrls = new string[] + { + "https://m.media-amazon.com/images/I/6170mJHIsYL._AC_SX466_.jpg", + "https://m.media-amazon.com/images/I/71a5As76MDL._AC_SX466_.jpg" + }, + CreatedDateUtc = DateTime.UtcNow, + WasOpened = false, + WishlistId = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab") + } + }; + + await _productCollection.InsertManyAsync(products); + } } diff --git a/ShoppingAssistantApi.Persistance/PersistanceExtentions/RepositoriesExtention.cs b/ShoppingAssistantApi.Persistance/PersistanceExtentions/RepositoriesExtention.cs index 3d076e3..3da2a49 100644 --- a/ShoppingAssistantApi.Persistance/PersistanceExtentions/RepositoriesExtention.cs +++ b/ShoppingAssistantApi.Persistance/PersistanceExtentions/RepositoriesExtention.cs @@ -16,6 +16,7 @@ public static class RepositoriesExtention services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/ShoppingAssistantApi.Persistance/Repositories/BaseRepository.cs b/ShoppingAssistantApi.Persistance/Repositories/BaseRepository.cs index dbfba85..8a128ff 100644 --- a/ShoppingAssistantApi.Persistance/Repositories/BaseRepository.cs +++ b/ShoppingAssistantApi.Persistance/Repositories/BaseRepository.cs @@ -81,4 +81,4 @@ public abstract class BaseRepository : IBaseRepository where T return await this._collection.FindOneAndUpdateAsync( Builders.Filter.Eq(e => e.Id, entity.Id), updateDefinition, options, cancellationToken); } -} \ No newline at end of file +} diff --git a/ShoppingAssistantApi.Persistance/Repositories/MessagesRepository.cs b/ShoppingAssistantApi.Persistance/Repositories/MessagesRepository.cs index 06481f6..55734c9 100644 --- a/ShoppingAssistantApi.Persistance/Repositories/MessagesRepository.cs +++ b/ShoppingAssistantApi.Persistance/Repositories/MessagesRepository.cs @@ -1,3 +1,5 @@ +using System.Linq.Expressions; +using MongoDB.Driver; using ShoppingAssistantApi.Application.IRepositories; using ShoppingAssistantApi.Domain.Entities; using ShoppingAssistantApi.Persistance.Database; @@ -7,4 +9,13 @@ namespace ShoppingAssistantApi.Persistance.Repositories; public class MessagesRepository : BaseRepository, IMessagesRepository { public MessagesRepository(MongoDbContext db) : base(db, "Messages") { } + + public async Task> GetPageStartingFromEndAsync(int pageNumber, int pageSize, Expression> predicate, CancellationToken cancellationToken) + { + return await _collection.Find(predicate) + .SortByDescending(x => x.CreatedDateUtc) + .Skip((pageNumber - 1) * pageSize) + .Limit(pageSize) + .ToListAsync(cancellationToken); + } } diff --git a/ShoppingAssistantApi.Persistance/Repositories/ProductRepository.cs b/ShoppingAssistantApi.Persistance/Repositories/ProductRepository.cs new file mode 100644 index 0000000..3e0863a --- /dev/null +++ b/ShoppingAssistantApi.Persistance/Repositories/ProductRepository.cs @@ -0,0 +1,10 @@ +using ShoppingAssistantApi.Application.IRepositories; +using ShoppingAssistantApi.Domain.Entities; +using ShoppingAssistantApi.Persistance.Database; + +namespace ShoppingAssistantApi.Persistance.Repositories; + +public class ProductsRepository : BaseRepository, IProductsRepository +{ + public ProductsRepository(MongoDbContext db) : base(db, "Products") { } +} diff --git a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs index ae2ed20..6877b07 100644 --- a/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs +++ b/ShoppingAssistantApi.Tests/TestExtentions/DbInitializer.cs @@ -97,29 +97,35 @@ public class DbInitializer public async Task InitializeWishlistsAsync() { var wishlistsCollection = _dbContext.Db.GetCollection("Wishlists"); + var messagesCollection = _dbContext.Db.GetCollection("Messages"); var gamingPcWishlist = new Wishlist { Id = ObjectId.Parse("ab79cde6f69abcd3efab65cd"), Name = "Gaming PC", Type = WishlistTypes.Product.ToString(), - CreatedById = ObjectId.Parse("652c3b89ae02a3135d6418fc"), - Messages = new Message[] - { - new Message - { - Text = "Prompt", - Role = MessageRoles.User.ToString(), - }, - new Message - { - Text = "Answer", - Role = MessageRoles.Application.ToString(), - }, - } + CreatedById = ObjectId.Parse("652c3b89ae02a3135d6418fc") }; await wishlistsCollection.InsertOneAsync(gamingPcWishlist); + await messagesCollection.InsertManyAsync(new Message[] + { + new() { + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd"), + Text = "Prompt", + Role = MessageRoles.User.ToString(), + CreatedDateUtc = DateTime.UtcNow.AddMinutes(-1), + CreatedById = ObjectId.Parse("652c3b89ae02a3135d6418fc") + }, + new() { + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab65cd"), + Text = "Answer", + Role = MessageRoles.Application.ToString(), + CreatedDateUtc = DateTime.UtcNow, + CreatedById = ObjectId.Parse("652c3b89ae02a3135d6418fc") + }, + }); + var genericWishlist = new Wishlist { Id = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), @@ -130,11 +136,55 @@ public class DbInitializer { new Message { - Text = "Prompt", + Text = "One Message", Role = MessageRoles.User.ToString(), + CreatedDateUtc = DateTime.UtcNow.AddMinutes(-1), + CreatedById = ObjectId.Parse("652c3b89ae02a3135d6409fc") } } }; await wishlistsCollection.InsertOneAsync(genericWishlist); + await messagesCollection.InsertOneAsync(new Message + { + WishlistId = ObjectId.Parse("ab6c2c2d9edf39abcd1ef9ab"), + Text = "One Message", + Role = MessageRoles.User.ToString(), + CreatedDateUtc = DateTime.UtcNow.AddMinutes(-1), + CreatedById = ObjectId.Parse("652c3b89ae02a3135d6409fc") + }); + + var mouseWishlist = new Wishlist + { + Id = ObjectId.Parse("ab79cde6f69abcd3efab95cd"), + Name = "Mouse", + Type = WishlistTypes.Product.ToString(), + CreatedById = ObjectId.Parse("652c3b89ae02a3135d6418fc"), + }; + await wishlistsCollection.InsertOneAsync(mouseWishlist); + + await messagesCollection.InsertManyAsync(new List + { + new() { + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab95cd"), + Text = "First Message", + Role = MessageRoles.User.ToString(), + CreatedDateUtc = DateTime.UtcNow.AddMinutes(-2), + CreatedById = ObjectId.Parse("652c3b89ae02a3135d6418fc"), + }, + new() { + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab95cd"), + Text = "Second Message", + Role = MessageRoles.Application.ToString(), + CreatedDateUtc = DateTime.UtcNow.AddMinutes(-1), + CreatedById = ObjectId.Parse("652c3b89ae02a3135d6418fc"), + }, + new() { + WishlistId = ObjectId.Parse("ab79cde6f69abcd3efab95cd"), + Text = "Third Message", + Role = MessageRoles.User.ToString(), + CreatedDateUtc = DateTime.UtcNow, + CreatedById = ObjectId.Parse("652c3b89ae02a3135d6418fc"), + }, + }); } } diff --git a/ShoppingAssistantApi.Tests/Tests/AccessTests.cs b/ShoppingAssistantApi.Tests/Tests/AccessTests.cs index 74f6cc1..0a5195c 100644 --- a/ShoppingAssistantApi.Tests/Tests/AccessTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/AccessTests.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using ShoppingAssistantApi.Application.Models.Identity; using ShoppingAssistantApi.Tests.TestExtentions; using Xunit; diff --git a/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs b/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs index 0f5b723..ac89a96 100644 --- a/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs +++ b/ShoppingAssistantApi.Tests/Tests/WishlistsTests.cs @@ -107,7 +107,7 @@ public class WishlistsTests : TestsBase } [Fact] - public async Task AddMessageToPersonalWishlist_ValidMessageModel_ReturnsNewMessageModel() + public async Task AddMessageToPersonalWishlist_ValidMessage_ReturnsNewMessage() { await LoginAsync(TestingUserEmail, TestingUserPassword); const string MessageText = "Second Message"; @@ -138,6 +138,41 @@ public class WishlistsTests : TestsBase Assert.Equal(TestingUserId, message.CreatedById); } + [Fact] + public async Task GetMessagesPageFromPersonalWishlist_ValidPageNumberAndSize_ReturnsPage() + { + await LoginAsync(TestingUserEmail, TestingUserPassword); + var mutation = new + { + query = @" + query messagesPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + messagesPageFromPersonalWishlist (wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { + hasNextPage, + hasPreviousPage, + items { id, text, role, createdById }, + pageNumber, + pageSize, + totalItems, + totalPages + } + }", + variables = new + { + wishlistId = "ab79cde6f69abcd3efab95cd", // From DbInitializer + pageNumber = 1, + pageSize = 2 + } + }; + + var jsonObject = await SendGraphQlRequestAsync(mutation); + var pagedList = (PagedList?) jsonObject?.data?.messagesPageFromPersonalWishlist?.ToObject>(); + + Assert.NotNull(pagedList); + Assert.NotEmpty(pagedList.Items); + Assert.Equal("Third Message", pagedList.Items.FirstOrDefault()?.Text); + Assert.Equal(MessageRoles.User.ToString(), pagedList.Items.FirstOrDefault()?.Role); + } + [Fact] public async Task StartPersonalWishlistAsync_InvalidWishlist_ReturnsErrors() { @@ -245,4 +280,4 @@ public class WishlistsTests : TestsBase Assert.NotNull(errors); Assert.True(errors.Count > 0); } -} +} \ No newline at end of file diff --git a/ShoppingAssistantApi.UnitTests/GlobalUsings.cs b/ShoppingAssistantApi.UnitTests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/ShoppingAssistantApi.UnitTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj b/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj new file mode 100644 index 0000000..9274a65 --- /dev/null +++ b/ShoppingAssistantApi.UnitTests/ShoppingAssistantApi.UnitTests.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/ShoppingAssistantApi.sln b/ShoppingAssistantApi.sln index fb54417..f2f2788 100644 --- a/ShoppingAssistantApi.sln +++ b/ShoppingAssistantApi.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShoppingAssistantApi.Api", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShoppingAssistantApi.Tests", "ShoppingAssistantApi.Tests\ShoppingAssistantApi.Tests.csproj", "{297B5378-79D7-406C-80A5-151C6B3EA147}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShoppingAssistantApi.UnitTests", "ShoppingAssistantApi.UnitTests\ShoppingAssistantApi.UnitTests.csproj", "{B4EFE8F1-89F5-44E4-BD0A-4F63D09C8E6F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {297B5378-79D7-406C-80A5-151C6B3EA147}.Debug|Any CPU.Build.0 = Debug|Any CPU {297B5378-79D7-406C-80A5-151C6B3EA147}.Release|Any CPU.ActiveCfg = Release|Any CPU {297B5378-79D7-406C-80A5-151C6B3EA147}.Release|Any CPU.Build.0 = Release|Any CPU + {B4EFE8F1-89F5-44E4-BD0A-4F63D09C8E6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4EFE8F1-89F5-44E4-BD0A-4F63D09C8E6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4EFE8F1-89F5-44E4-BD0A-4F63D09C8E6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4EFE8F1-89F5-44E4-BD0A-4F63D09C8E6F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE