using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Services.Exceptions; namespace Timeline.Services { [Serializable] public class InvalidBookmarkException : Exception { public InvalidBookmarkException() { } public InvalidBookmarkException(string message) : base(message) { } public InvalidBookmarkException(string message, Exception inner) : base(message, inner) { } protected InvalidBookmarkException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } } /// /// Service interface that manages timeline bookmarks. /// public interface IBookmarkTimelineService { /// /// Get bookmarks of a user. /// /// User id of bookmark owner. /// Id of Bookmark timelines in order. /// Thrown when user does not exist. Task> GetBookmarks(long userId); /// /// Add a bookmark to tail to a user. /// /// User id of bookmark owner. /// Timeline name. /// Thrown when is null. /// Thrown when is not a valid name. /// Thrown when user does not exist. /// Thrown when timeline does not exist. Task AddBookmark(long userId, string timelineName); /// /// Remove a bookmark from a user. /// /// User id of bookmark owner. /// Timeline name. /// True if deletion is performed. False if bookmark does not exist. /// Thrown when is null. /// Thrown when is not a valid name. /// Thrown when user does not exist. /// Thrown when timeline does not exist. Task RemoveBookmark(long userId, string timelineName); /// /// Move bookmark to a new position. /// /// User id of bookmark owner. /// Timeline name. /// New position. Starts at 1. /// Thrown when is null. /// Thrown when is not a valid name. /// Thrown when user does not exist. /// Thrown when timeline does not exist. /// Thrown when the timeline is not a bookmark. Task MoveBookmark(long userId, string timelineName, long newPosition); } public class BookmarkTimelineService : IBookmarkTimelineService { private readonly DatabaseContext _database; private readonly IBasicUserService _userService; private readonly IBasicTimelineService _timelineService; public BookmarkTimelineService(DatabaseContext database, IBasicUserService userService, IBasicTimelineService timelineService) { _database = database; _userService = userService; _timelineService = timelineService; } public async Task AddBookmark(long userId, string timelineName) { if (timelineName is null) throw new ArgumentNullException(nameof(timelineName)); if (!await _userService.CheckUserExistence(userId)) throw new UserNotExistException(userId); var timelineId = await _timelineService.GetTimelineIdByName(timelineName); if (await _database.BookmarkTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId) is not null) { return; } _database.BookmarkTimelines.Add(new BookmarkTimelineEntity { TimelineId = timelineId, UserId = userId, Rank = (await _database.BookmarkTimelines.CountAsync(t => t.UserId == userId)) + 1 }); await _database.SaveChangesAsync(); } public async Task> GetBookmarks(long userId) { if (!await _userService.CheckUserExistence(userId)) throw new UserNotExistException(userId); var entities = await _database.BookmarkTimelines.Where(t => t.UserId == userId).OrderBy(t => t.Rank).Select(t => new { t.TimelineId }).ToListAsync(); return entities.Select(e => e.TimelineId).ToList(); } public async Task MoveBookmark(long userId, string timelineName, long newPosition) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); var timelineId = await _timelineService.GetTimelineIdByName(timelineName); var entity = await _database.BookmarkTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId && t.UserId == userId); if (entity == null) throw new InvalidBookmarkException("You can't move a non-bookmark timeline."); var oldPosition = entity.Rank; if (newPosition < 1) { newPosition = 1; } else { var totalCount = await _database.BookmarkTimelines.CountAsync(t => t.UserId == userId); if (newPosition > totalCount) newPosition = totalCount; } if (oldPosition == newPosition) return; await using var transaction = await _database.Database.BeginTransactionAsync(); if (newPosition > oldPosition) { await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` - 1 WHERE `rank` BETWEEN {0} AND {1} AND `user` = {2}", oldPosition + 1, newPosition, userId); await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = {0} WHERE `id` = {1}", newPosition, entity.Id); } else { await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` + 1 WHERE `rank` BETWEEN {0} AND {1} AND `user` = {2}", newPosition, oldPosition - 1, userId); await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = {0} WHERE `id` = {1}", newPosition, entity.Id); } await transaction.CommitAsync(); } public async Task RemoveBookmark(long userId, string timelineName) { if (timelineName is null) throw new ArgumentNullException(nameof(timelineName)); if (!await _userService.CheckUserExistence(userId)) throw new UserNotExistException(userId); var timelineId = await _timelineService.GetTimelineIdByName(timelineName); var entity = await _database.BookmarkTimelines.SingleOrDefaultAsync(t => t.UserId == userId && t.TimelineId == timelineId); if (entity == null) return false; await using var transaction = await _database.Database.BeginTransactionAsync(); var rank = entity.Rank; _database.BookmarkTimelines.Remove(entity); await _database.SaveChangesAsync(); await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` - 1 WHERE `rank` > {0}", rank); await transaction.CommitAsync(); return true; } } }