From 3327f59c25138c66ecb637a0a90ce8ccc58db2e4 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 18 Dec 2020 14:56:02 +0800 Subject: feat(database): Add bookmark timeline database entity. --- .../Timeline/Entities/BookmarkTimelineEntity.cs | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 BackEnd/Timeline/Entities/BookmarkTimelineEntity.cs diff --git a/BackEnd/Timeline/Entities/BookmarkTimelineEntity.cs b/BackEnd/Timeline/Entities/BookmarkTimelineEntity.cs new file mode 100644 index 00000000..99c0bc9b --- /dev/null +++ b/BackEnd/Timeline/Entities/BookmarkTimelineEntity.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("bookmark_timelines")] + public class BookmarkTimelineEntity + { + [Key, Column("id"), DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("timeline")] + public long TimelineId { get; set; } + + [ForeignKey(nameof(TimelineId))] + public TimelineEntity Timeline { get; set; } = default!; + + [Column("user")] + public long UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public UserEntity User { get; set; } = default!; + + // I don't use order any more since keyword name conflict. + [Column("rank")] + public long Rank { get; set; } + } +} -- cgit v1.2.3 From 12e94f1ee5cd34d0dfc2db4f971d0de78fa84c06 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 18 Dec 2020 18:01:25 +0800 Subject: feat: Add bookmark timeline service interface. --- .../Timeline/Services/BookmarkTimelineService.cs | 74 ++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 BackEnd/Timeline/Services/BookmarkTimelineService.cs diff --git a/BackEnd/Timeline/Services/BookmarkTimelineService.cs b/BackEnd/Timeline/Services/BookmarkTimelineService.cs new file mode 100644 index 00000000..7eb691b7 --- /dev/null +++ b/BackEnd/Timeline/Services/BookmarkTimelineService.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Timeline.Models; +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. + /// Bookmarks 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 position); + } + + public class BookmarkTimelineService + { + } +} -- cgit v1.2.3 From 53291d2c16047d3eb4c5eeb9a216c106cf47ead3 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 18 Dec 2020 19:26:50 +0800 Subject: feat: Implement bookmark service. --- BackEnd/Timeline/Entities/DatabaseContext.cs | 1 + .../Timeline/Services/BookmarkTimelineService.cs | 127 ++++++++++++++++++++- 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/BackEnd/Timeline/Entities/DatabaseContext.cs b/BackEnd/Timeline/Entities/DatabaseContext.cs index 4205c2cf..513cdc95 100644 --- a/BackEnd/Timeline/Entities/DatabaseContext.cs +++ b/BackEnd/Timeline/Entities/DatabaseContext.cs @@ -30,6 +30,7 @@ namespace Timeline.Entities public DbSet TimelinePosts { get; set; } = default!; public DbSet TimelineMembers { get; set; } = default!; public DbSet HighlightTimelines { get; set; } = default!; + public DbSet BookmarkTimelines { get; set; } = default!; public DbSet JwtToken { get; set; } = default!; public DbSet Data { get; set; } = default!; diff --git a/BackEnd/Timeline/Services/BookmarkTimelineService.cs b/BackEnd/Timeline/Services/BookmarkTimelineService.cs index 7eb691b7..f65a1ff0 100644 --- a/BackEnd/Timeline/Services/BookmarkTimelineService.cs +++ b/BackEnd/Timeline/Services/BookmarkTimelineService.cs @@ -1,6 +1,9 @@ -using System; +using Microsoft.EntityFrameworkCore; +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Timeline.Entities; using Timeline.Models; using Timeline.Services.Exceptions; @@ -59,16 +62,132 @@ namespace Timeline.Services /// /// User id of bookmark owner. /// Timeline name. - /// New position. Starts at 1. + /// 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 position); + Task MoveBookmark(long userId, string timelineName, long newPosition); } - public class BookmarkTimelineService + public class BookmarkTimelineService : IBookmarkTimelineService { + private readonly DatabaseContext _database; + private readonly IBasicUserService _userService; + private readonly ITimelineService _timelineService; + + public BookmarkTimelineService(DatabaseContext database, IBasicUserService userService, ITimelineService 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); + + _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).Select(t => new { t.TimelineId }).ToListAsync(); + + List result = new(); + + foreach (var entity in entities) + { + result.Add(await _timelineService.GetTimelineById(entity.TimelineId)); + } + + return result; + } + + 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.HighlightTimelines.CountAsync(); + 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; + } } } -- cgit v1.2.3 From ab3aedad37fe4634efb0d6939d7a40642bfff032 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 18 Dec 2020 22:40:22 +0800 Subject: feat: Bookmark timeline service unit tests. --- .../Services/BookmarkTimelineServiceTest.cs | 89 ++++++++++++++++++++++ .../Services/HighlightTimelineServiceTest.cs | 4 + .../Timeline/Services/BookmarkTimelineService.cs | 4 +- 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 BackEnd/Timeline.Tests/Services/BookmarkTimelineServiceTest.cs diff --git a/BackEnd/Timeline.Tests/Services/BookmarkTimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/BookmarkTimelineServiceTest.cs new file mode 100644 index 00000000..1b8bff63 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/BookmarkTimelineServiceTest.cs @@ -0,0 +1,89 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using System.Threading.Tasks; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.Services +{ + public class BookmarkTimelineServiceTest : DatabaseBasedTest + { + private BookmarkTimelineService _service = default!; + private UserService _userService = default!; + private TimelineService _timelineService = default!; + + protected override void OnDatabaseCreated() + { + var clock = new TestClock(); + _userService = new UserService(NullLogger.Instance, Database, new PasswordService(), new UserPermissionService(Database), clock); + _timelineService = new TimelineService(Database, _userService, clock); + _service = new BookmarkTimelineService(Database, _userService, _timelineService); + } + + [Fact] + public async Task Should_Work() + { + var userId = await _userService.GetUserIdByUsername("user"); + + { + var b = await _service.GetBookmarks(userId); + b.Should().BeEmpty(); + } + + await _timelineService.CreateTimeline("tl", userId); + await _service.AddBookmark(userId, "tl"); + + { + var b = await _service.GetBookmarks(userId); + b.Should().HaveCount(1).And.BeEquivalentTo(await _timelineService.GetTimeline("tl")); + } + } + + [Fact] + public async Task NewOne_Should_BeAtLast() + { + var userId = await _userService.GetUserIdByUsername("user"); + await _timelineService.CreateTimeline("t1", userId); + await _service.AddBookmark(userId, "t1"); + + await _timelineService.CreateTimeline("t2", userId); + await _service.AddBookmark(userId, "t2"); + + var b = await _service.GetBookmarks(userId); + + b.Should().HaveCount(2); + b[0].Name.Should().Be("t1"); + b[1].Name.Should().Be("t2"); + } + + [Fact] + public async Task Multiple_Should_Work() + { + var userId = await _userService.GetUserIdByUsername("user"); + + // make timeline id not same as entity id. + await _timelineService.CreateTimeline("t0", userId); + + await _timelineService.CreateTimeline("t1", userId); + await _service.AddBookmark(userId, "t1"); + + await _timelineService.CreateTimeline("t2", userId); + await _service.AddBookmark(userId, "t2"); + + await _timelineService.CreateTimeline("t3", userId); + await _service.AddBookmark(userId, "t3"); + + await _service.MoveBookmark(userId, "t3", 2); + (await _service.GetBookmarks(userId))[1].Name.Should().Be("t3"); + + await _service.MoveBookmark(userId, "t1", 3); + (await _service.GetBookmarks(userId))[2].Name.Should().Be("t1"); + + await _service.RemoveBookmark(userId, "t2"); + await _service.RemoveBookmark(userId, "t1"); + await _service.RemoveBookmark(userId, "t3"); + (await _service.GetBookmarks(userId)).Should().BeEmpty(); + } + } +} diff --git a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs index dca070c6..f48404a9 100644 --- a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs @@ -68,6 +68,10 @@ namespace Timeline.Tests.Services public async Task Multiple_Should_Work() { var userId = await _userService.GetUserIdByUsername("user"); + + // make timeline id not same as entity id. + await _timelineService.CreateTimeline("t0", userId); + await _timelineService.CreateTimeline("t1", userId); await _service.AddHighlightTimeline("t1", userId); diff --git a/BackEnd/Timeline/Services/BookmarkTimelineService.cs b/BackEnd/Timeline/Services/BookmarkTimelineService.cs index f65a1ff0..09438193 100644 --- a/BackEnd/Timeline/Services/BookmarkTimelineService.cs +++ b/BackEnd/Timeline/Services/BookmarkTimelineService.cs @@ -109,7 +109,7 @@ namespace Timeline.Services if (!await _userService.CheckUserExistence(userId)) throw new UserNotExistException(userId); - var entities = await _database.BookmarkTimelines.Where(t => t.UserId == userId).Select(t => new { t.TimelineId }).ToListAsync(); + var entities = await _database.BookmarkTimelines.Where(t => t.UserId == userId).OrderBy(t => t.Rank).Select(t => new { t.TimelineId }).ToListAsync(); List result = new(); @@ -140,7 +140,7 @@ namespace Timeline.Services } else { - var totalCount = await _database.HighlightTimelines.CountAsync(); + var totalCount = await _database.BookmarkTimelines.CountAsync(t => t.UserId == userId); if (newPosition > totalCount) newPosition = totalCount; } -- cgit v1.2.3 From cf8a869de33cfa5db1967698059abccaaeaba4c9 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 19 Dec 2020 20:08:49 +0800 Subject: feat: Bookmark timeline REST api. --- BackEnd/Timeline.ErrorCodes/ErrorCodes.cs | 5 + .../IntegratedTests/BookmarkTimelineTest.cs | 87 ++++++++++++++++ .../IntegratedTests/HighlightTimelineTest.cs | 1 + .../IntegratedTests/HttpClientTestExtensions.cs | 5 + .../Controllers/BookmarkTimelineController.cs | 114 +++++++++++++++++++++ .../Controllers/HighlightTimelineController.cs | 8 +- BackEnd/Timeline/Models/Http/BookmarkTimeline.cs | 23 +++++ BackEnd/Timeline/Models/Http/HighlightTimeline.cs | 6 ++ BackEnd/Timeline/Startup.cs | 1 + 9 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/BookmarkTimelineTest.cs create mode 100644 BackEnd/Timeline/Controllers/BookmarkTimelineController.cs create mode 100644 BackEnd/Timeline/Models/Http/BookmarkTimeline.cs diff --git a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs index a8519216..c65bf26e 100644 --- a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs +++ b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs @@ -68,6 +68,11 @@ { public const int NonHighlight = 1_105_01_01; } + + public static class BookmarkTimelineController + { + public const int NonBookmark = 1_106_01_01; + } } } diff --git a/BackEnd/Timeline.Tests/IntegratedTests/BookmarkTimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/BookmarkTimelineTest.cs new file mode 100644 index 00000000..e6ae178f --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/BookmarkTimelineTest.cs @@ -0,0 +1,87 @@ +using FluentAssertions; +using System.Collections.Generic; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class BookmarkTimelineTest : IntegratedTestBase + { + [Fact] + public async Task AuthTest() + { + using var client = await CreateDefaultClient(); + + await client.TestPutAssertUnauthorizedAsync("bookmarks/@user1"); + await client.TestDeleteAssertUnauthorizedAsync("bookmarks/@user1"); + await client.TestPostAssertUnauthorizedAsync("bookmarkop/move", new HttpBookmarkTimelineMoveRequest { Timeline = "aaa", NewPosition = 1 }); + } + + [Fact] + public async Task InvalidModel() + { + using var client = await CreateClientAsUser(); + + await client.TestPutAssertInvalidModelAsync("bookmarks/!!!"); + await client.TestDeleteAssertInvalidModelAsync("bookmarks/!!!"); + await client.TestPostAssertInvalidModelAsync("bookmarkop/move", new HttpBookmarkTimelineMoveRequest { Timeline = null!, NewPosition = 1 }); + await client.TestPostAssertInvalidModelAsync("bookmarkop/move", new HttpBookmarkTimelineMoveRequest { Timeline = "!!!", NewPosition = 1 }); + await client.TestPostAssertInvalidModelAsync("bookmarkop/move", new HttpBookmarkTimelineMoveRequest { Timeline = "aaa", NewPosition = null }); + } + + [Fact] + public async Task ShouldWork() + { + using var client = await CreateClientAsUser(); + await client.TestPostAsync("timelines", new TimelineCreateRequest { Name = "t1" }); + + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().BeEmpty(); + } + + await client.TestPutAsync("bookmarks/@user1"); + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().HaveCount(1); + h[0].Name.Should().Be("@user1"); + } + + await client.TestPutAsync("bookmarks/t1"); + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().HaveCount(2); + h[0].Name.Should().Be("@user1"); + h[1].Name.Should().Be("t1"); + } + + await client.TestPostAsync("bookmarkop/move", new HttpHighlightTimelineMoveRequest { Timeline = "@user1", NewPosition = 2 }); + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().HaveCount(2); + h[0].Name.Should().Be("t1"); + h[1].Name.Should().Be("@user1"); + } + + await client.TestDeleteAsync("bookmarks/@user1"); + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().HaveCount(1); + h[0].Name.Should().Be("t1"); + } + + await client.TestDeleteAsync("bookmarks/t1"); + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().BeEmpty(); + } + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs index d4b4d55d..63f40a1e 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs @@ -28,6 +28,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestPutAssertInvalidModelAsync("highlights/!!!"); await client.TestDeleteAssertInvalidModelAsync("highlights/!!!"); await client.TestPostAssertInvalidModelAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = null!, NewPosition = 1 }); + await client.TestPostAssertInvalidModelAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = "!!!", NewPosition = 1 }); await client.TestPostAssertInvalidModelAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = "aaa", NewPosition = null }); } diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs index ec517362..b219f092 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs @@ -192,6 +192,11 @@ namespace Timeline.Tests.IntegratedTests await client.TestJsonSendAssertUnauthorizedAsync(HttpMethod.Patch, url, jsonBody, errorCode, headerSetup); } + public static async Task TestPutAssertUnauthorizedAsync(this HttpClient client, string url, object? jsonBody = null, int? errorCode = null, HeaderSetup? headerSetup = null) + { + await client.TestJsonSendAssertUnauthorizedAsync(HttpMethod.Put, url, jsonBody, errorCode, headerSetup); + } + public static async Task TestDeleteAssertUnauthorizedAsync(this HttpClient client, string url, object? jsonBody = null, int? errorCode = null, HeaderSetup? headerSetup = null) { await client.TestJsonSendAssertUnauthorizedAsync(HttpMethod.Delete, url, jsonBody, errorCode, headerSetup); diff --git a/BackEnd/Timeline/Controllers/BookmarkTimelineController.cs b/BackEnd/Timeline/Controllers/BookmarkTimelineController.cs new file mode 100644 index 00000000..9dff95f3 --- /dev/null +++ b/BackEnd/Timeline/Controllers/BookmarkTimelineController.cs @@ -0,0 +1,114 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Services.Exceptions; + +namespace Timeline.Controllers +{ + /// + /// Api related to timeline bookmarks. + /// + [ApiController] + [ProducesErrorResponseType(typeof(CommonResponse))] + public class BookmarkTimelineController : Controller + { + private readonly IBookmarkTimelineService _service; + + private readonly IMapper _mapper; + + public BookmarkTimelineController(IBookmarkTimelineService service, IMapper mapper) + { + _service = service; + _mapper = mapper; + } + + /// + /// Get bookmark list in order. + /// + /// Bookmarks. + [HttpGet("bookmarks")] + [Authorize] + [ProducesResponseType(200)] + [ProducesResponseType(401)] + public async Task>> List() + { + var bookmarks = await _service.GetBookmarks(this.GetUserId()); + return Ok(_mapper.Map>(bookmarks)); + } + + /// + /// Add a bookmark. + /// + /// Timeline name. + [HttpPut("bookmarks/{timeline}")] + [Authorize] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + public async Task Put([GeneralTimelineName] string timeline) + { + try + { + await _service.AddBookmark(this.GetUserId(), timeline); + return Ok(); + } + catch (TimelineNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.NotExist()); + } + } + + /// + /// Remove a bookmark. + /// + /// Timeline name. + [HttpDelete("bookmarks/{timeline}")] + [Authorize] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + public async Task Delete([GeneralTimelineName] string timeline) + { + try + { + await _service.RemoveBookmark(this.GetUserId(), timeline); + return Ok(); + } + catch (TimelineNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.NotExist()); + } + } + + /// + /// Move a bookmark to new posisition. + /// + /// Request body. + [HttpPost("bookmarkop/move")] + [Authorize] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + public async Task Move([FromBody] HttpBookmarkTimelineMoveRequest request) + { + try + { + await _service.MoveBookmark(this.GetUserId(), request.Timeline, request.NewPosition!.Value); + return Ok(); + } + catch (TimelineNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.NotExist()); + } + catch (InvalidBookmarkException) + { + return BadRequest(new CommonResponse(ErrorCodes.BookmarkTimelineController.NonBookmark, "You can't move a non-bookmark timeline.")); + } + } + } +} diff --git a/BackEnd/Timeline/Controllers/HighlightTimelineController.cs b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs index 0b6e1665..519d6161 100644 --- a/BackEnd/Timeline/Controllers/HighlightTimelineController.cs +++ b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs @@ -46,6 +46,8 @@ namespace Timeline.Controllers [PermissionAuthorize(UserPermission.HighlightTimelineManagement)] [ProducesResponseType(200)] [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(403)] public async Task Put([GeneralTimelineName] string timeline) { try @@ -67,6 +69,8 @@ namespace Timeline.Controllers [PermissionAuthorize(UserPermission.HighlightTimelineManagement)] [ProducesResponseType(200)] [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(403)] public async Task Delete([GeneralTimelineName] string timeline) { try @@ -81,12 +85,14 @@ namespace Timeline.Controllers } /// - /// Move a highlight position. + /// Move a highlight to new position. /// [HttpPost("highlightop/move")] [PermissionAuthorize(UserPermission.HighlightTimelineManagement)] [ProducesResponseType(200)] [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(403)] public async Task Move([FromBody] HttpHighlightTimelineMoveRequest body) { try diff --git a/BackEnd/Timeline/Models/Http/BookmarkTimeline.cs b/BackEnd/Timeline/Models/Http/BookmarkTimeline.cs new file mode 100644 index 00000000..14be1112 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/BookmarkTimeline.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + /// + /// Move bookmark timeline request body model. + /// + public class HttpBookmarkTimelineMoveRequest + { + /// + /// Timeline name. + /// + [GeneralTimelineName] + public string Timeline { get; set; } = default!; + + /// + /// New position, starting at 1. + /// + [Required] + public long? NewPosition { get; set; } + } +} diff --git a/BackEnd/Timeline/Models/Http/HighlightTimeline.cs b/BackEnd/Timeline/Models/Http/HighlightTimeline.cs index e5aed068..5af0e528 100644 --- a/BackEnd/Timeline/Models/Http/HighlightTimeline.cs +++ b/BackEnd/Timeline/Models/Http/HighlightTimeline.cs @@ -8,9 +8,15 @@ namespace Timeline.Models.Http /// public class HttpHighlightTimelineMoveRequest { + /// + /// Timeline name. + /// [GeneralTimelineName] public string Timeline { get; set; } = default!; + /// + /// New position, starting at 1. + /// [Required] public long? NewPosition { get; set; } } diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index d20fc54b..66c708ac 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -102,6 +102,7 @@ namespace Timeline services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddDbContext((services, options) => { -- cgit v1.2.3 From 682b5a076c967f9f38dd32c0cffd4010548bd400 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 19 Dec 2020 20:09:52 +0800 Subject: feat(database): Add migration, --- .../20201219120929_AddBookmarkTimeline.Designer.cs | 498 +++++++++++++++++++++ .../20201219120929_AddBookmarkTimeline.cs | 53 +++ .../Migrations/DatabaseContextModelSnapshot.cs | 47 ++ 3 files changed, 598 insertions(+) create mode 100644 BackEnd/Timeline/Migrations/20201219120929_AddBookmarkTimeline.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20201219120929_AddBookmarkTimeline.cs diff --git a/BackEnd/Timeline/Migrations/20201219120929_AddBookmarkTimeline.Designer.cs b/BackEnd/Timeline/Migrations/20201219120929_AddBookmarkTimeline.Designer.cs new file mode 100644 index 00000000..a68decb8 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20201219120929_AddBookmarkTimeline.Designer.cs @@ -0,0 +1,498 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20201219120929_AddBookmarkTimeline")] + partial class AddBookmarkTimeline + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.0"); + + modelBuilder.Entity("Timeline.Entities.BookmarkTimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Rank") + .HasColumnType("INTEGER") + .HasColumnName("rank"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("bookmark_timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property("Ref") + .HasColumnType("INTEGER") + .HasColumnName("ref"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tag"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("AddTime") + .HasColumnType("TEXT") + .HasColumnName("add_time"); + + b.Property("OperatorId") + .HasColumnType("INTEGER") + .HasColumnName("operator_id"); + + b.Property("Order") + .HasColumnType("INTEGER") + .HasColumnName("order"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline_id"); + + b.HasKey("Id"); + + b.HasIndex("OperatorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("highlight_timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Key") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("key"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreateTime") + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("CurrentPostLocalId") + .HasColumnType("INTEGER") + .HasColumnName("current_post_local_id"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("LastModified") + .HasColumnType("TEXT") + .HasColumnName("last_modified"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("NameLastModified") + .HasColumnType("TEXT") + .HasColumnName("name_last_modified"); + + b.Property("OwnerId") + .HasColumnType("INTEGER") + .HasColumnName("owner"); + + b.Property("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("unique_id") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnType("INTEGER") + .HasColumnName("visibility"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("INTEGER") + .HasColumnName("author"); + + b.Property("Content") + .HasColumnType("TEXT") + .HasColumnName("content"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("content_type"); + + b.Property("ExtraContent") + .HasColumnType("TEXT") + .HasColumnName("extra_content"); + + b.Property("LastUpdated") + .HasColumnType("TEXT") + .HasColumnName("last_updated"); + + b.Property("LocalId") + .HasColumnType("INTEGER") + .HasColumnName("local_id"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("DataTag") + .HasColumnType("TEXT") + .HasColumnName("data_tag"); + + b.Property("LastModified") + .HasColumnType("TEXT") + .HasColumnName("last_modified"); + + b.Property("Type") + .HasColumnType("TEXT") + .HasColumnName("type"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreateTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("create_time") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("last_modified") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Nickname") + .HasColumnType("TEXT") + .HasColumnName("nickname"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("password"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("unique_id") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("username"); + + b.Property("UsernameChangeTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("username_change_time") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0L) + .HasColumnName("version"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.UserPermissionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Permission") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("permission"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_permission"); + }); + + modelBuilder.Entity("Timeline.Entities.BookmarkTimelineEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany() + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Timeline"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Operator") + .WithMany() + .HasForeignKey("OperatorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany() + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Operator"); + + b.Navigation("Timeline"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Timeline"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Timeline"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.UserPermissionEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Navigation("Members"); + + b.Navigation("Posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Navigation("Avatar"); + + b.Navigation("Permissions"); + + b.Navigation("TimelinePosts"); + + b.Navigation("Timelines"); + + b.Navigation("TimelinesJoined"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20201219120929_AddBookmarkTimeline.cs b/BackEnd/Timeline/Migrations/20201219120929_AddBookmarkTimeline.cs new file mode 100644 index 00000000..571d0419 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20201219120929_AddBookmarkTimeline.cs @@ -0,0 +1,53 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class AddBookmarkTimeline : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "bookmark_timelines", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + timeline = table.Column(type: "INTEGER", nullable: false), + user = table.Column(type: "INTEGER", nullable: false), + rank = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_bookmark_timelines", x => x.id); + table.ForeignKey( + name: "FK_bookmark_timelines_timelines_timeline", + column: x => x.timeline, + principalTable: "timelines", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_bookmark_timelines_users_user", + column: x => x.user, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_bookmark_timelines_timeline", + table: "bookmark_timelines", + column: "timeline"); + + migrationBuilder.CreateIndex( + name: "IX_bookmark_timelines_user", + table: "bookmark_timelines", + column: "user"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "bookmark_timelines"); + } + } +} diff --git a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs index ea3378dc..6b547a55 100644 --- a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -16,6 +16,34 @@ namespace Timeline.Migrations modelBuilder .HasAnnotation("ProductVersion", "5.0.0"); + modelBuilder.Entity("Timeline.Entities.BookmarkTimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Rank") + .HasColumnType("INTEGER") + .HasColumnName("rank"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("bookmark_timelines"); + }); + modelBuilder.Entity("Timeline.Entities.DataEntity", b => { b.Property("Id") @@ -338,6 +366,25 @@ namespace Timeline.Migrations b.ToTable("user_permission"); }); + modelBuilder.Entity("Timeline.Entities.BookmarkTimelineEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany() + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Timeline"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b => { b.HasOne("Timeline.Entities.UserEntity", "Operator") -- cgit v1.2.3