From 66658abde1220a53d0e022aaac8dd49a15034a34 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 4 Feb 2021 22:03:08 +0800 Subject: ... --- BackEnd/Timeline/Services/TimelinePostService.cs | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'BackEnd/Timeline/Services/TimelinePostService.cs') diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index c2b773ff..cf5f4e55 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -44,6 +44,17 @@ namespace Timeline.Services /// Thrown when timeline does not exist. Task> GetPosts(long timelineId, DateTime? modifiedSince = null, bool includeDeleted = false); + /// + /// Get a post of a timeline. + /// + /// The id of the timeline of the post. + /// The id of the post. + /// If true, return the entity even if it is deleted. + /// The post. + /// Thrown when timeline does not exist. + /// Thrown when post of does not exist or has been deleted. + Task GetPost(long timelineId, long postId, bool includeDelete = false); + /// /// Get the etag of data of a post. /// @@ -189,6 +200,25 @@ namespace Timeline.Services return await query.ToListAsync(); } + public async Task GetPost(long timelineId, long postId, bool includeDelete = false) + { + await CheckTimelineExistence(timelineId); + + var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (post is null) + { + throw new TimelinePostNotExistException(timelineId, postId, false); + } + + if (!includeDelete && post.Content is null) + { + throw new TimelinePostNotExistException(timelineId, postId, true); + } + + return post; + } + public async Task GetPostDataETag(long timelineId, long postId) { await CheckTimelineExistence(timelineId); -- cgit v1.2.3 From 204cb9a59494471804fe17a4a15f6856b531bb54 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 6 Feb 2021 17:43:03 +0800 Subject: ... --- .../Timeline/Controllers/TimelinePostController.cs | 77 ++++++----- .../Timeline/Models/TimelinePostContentTypes.cs | 3 + BackEnd/Timeline/Services/TimelinePostService.cs | 141 ++++++++++++--------- 3 files changed, 118 insertions(+), 103 deletions(-) (limited to 'BackEnd/Timeline/Services/TimelinePostService.cs') diff --git a/BackEnd/Timeline/Controllers/TimelinePostController.cs b/BackEnd/Timeline/Controllers/TimelinePostController.cs index 0148f56e..4dab2a44 100644 --- a/BackEnd/Timeline/Controllers/TimelinePostController.cs +++ b/BackEnd/Timeline/Controllers/TimelinePostController.cs @@ -162,54 +162,51 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - var content = body.Content; + var requestContent = body.Content; - TimelinePostEntity post; + TimelinePostCreateRequestContent createContent; - TimelinePostCommonProperties properties = new TimelinePostCommonProperties { Color = body.Color, Time = body.Time }; - - if (content.Type == TimelinePostContentTypes.Text) + switch (requestContent.Type) { - var text = content.Text; - if (text == null) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); - } - post = await _postService.CreateTextPost(timelineId, userId, text, properties); + case TimelinePostContentTypes.Text: + if (requestContent.Text is null) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); + } + createContent = new TimelinePostCreateRequestTextContent(requestContent.Text); + break; + case TimelinePostContentTypes.Image: + if (requestContent.Data is null) + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired)); + + // decode base64 + byte[] data; + try + { + data = Convert.FromBase64String(requestContent.Data); + } + catch (FormatException) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64)); + } + + createContent = new TimelinePostCreateRequestImageContent(data); + break; + default: + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); + } - else if (content.Type == TimelinePostContentTypes.Image) - { - var base64Data = content.Data; - if (base64Data == null) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired)); - } - byte[] data; - try - { - data = Convert.FromBase64String(base64Data); - } - catch (FormatException) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64)); - } - try - { - post = await _postService.CreateImagePost(timelineId, userId, data, properties); - } - catch (ImageException) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage)); - } + try + { + var post = await _postService.CreatePost(timelineId, userId, new TimelinePostCreateRequest(createContent) { Time = body.Time, Color = body.Color }); + var result = await _timelineMapper.MapToHttp(post, timeline, Url); + return result; } - else + catch (ImageException) { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage)); } - - var result = await _timelineMapper.MapToHttp(post, timeline, Url); - return result; } /// diff --git a/BackEnd/Timeline/Models/TimelinePostContentTypes.cs b/BackEnd/Timeline/Models/TimelinePostContentTypes.cs index 22763eba..ca5e79e1 100644 --- a/BackEnd/Timeline/Models/TimelinePostContentTypes.cs +++ b/BackEnd/Timeline/Models/TimelinePostContentTypes.cs @@ -4,7 +4,10 @@ namespace Timeline.Models { public static class TimelinePostContentTypes { +#pragma warning disable CA1819 // Properties should not return arrays public static string[] AllTypes { get; } = new string[] { Text, Image }; +#pragma warning restore CA1819 // Properties should not return arrays + public const string Text = "text"; public const string Image = "image"; } diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index cf5f4e55..076c45e8 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -24,12 +24,50 @@ namespace Timeline.Services public DateTime? LastModified { get; set; } // TODO: Why nullable? } - public class TimelinePostCommonProperties + public abstract class TimelinePostCreateRequestContent { + public abstract string TypeName { get; } + } + + public class TimelinePostCreateRequestTextContent : TimelinePostCreateRequestContent + { + public TimelinePostCreateRequestTextContent(string text) + { + Text = text; + } + + public override string TypeName => TimelinePostContentTypes.Text; + + public string Text { get; set; } + } + + public class TimelinePostCreateRequestImageContent : TimelinePostCreateRequestContent + { + public TimelinePostCreateRequestImageContent(byte[] data) + { + Data = data; + } + + public override string TypeName => TimelinePostContentTypes.Image; + +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } +#pragma warning restore CA1819 // Properties should not return arrays + } + + public class TimelinePostCreateRequest + { + public TimelinePostCreateRequest(TimelinePostCreateRequestContent content) + { + Content = content; + } + public string? Color { get; set; } /// If not set, current time is used. public DateTime? Time { get; set; } + + public TimelinePostCreateRequestContent Content { get; set; } } public interface ITimelinePostService @@ -79,31 +117,18 @@ namespace Timeline.Services Task GetPostData(long timelineId, long postId); /// - /// Create a new text post in timeline. + /// Create a new post in timeline. /// /// The id of the timeline to create post against. /// The author's user id. - /// The content text. - /// Some properties. + /// Info about the post. /// The info of the created post. - /// Thrown when is null. - /// Thrown when timeline does not exist. - /// Thrown if user of does not exist. - Task CreateTextPost(long timelineId, long authorId, string text, TimelinePostCommonProperties? properties = null); - - /// - /// Create a new image post in timeline. - /// - /// The id of the timeline to create post against. - /// The author's user id. - /// The image data. - /// Some properties. - /// The info of the created post. - /// Thrown when is null. + /// Thrown when is null. + /// Thrown when is of invalid format. /// Thrown when timeline does not exist. /// Thrown if user of does not exist. /// Thrown if data is not a image. Validated by . - Task CreateImagePost(long timelineId, long authorId, byte[] imageData, TimelinePostCommonProperties? properties = null); + Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request); /// /// Delete a post. @@ -284,22 +309,27 @@ namespace Timeline.Services }; } - private async Task GeneralCreatePost(long timelineId, long authorId, TimelinePostCommonProperties? properties, Func saveContent) + public async Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request) { - if (properties is not null) + if (request is null) + throw new ArgumentNullException(nameof(request)); + + + if (request.Content is null) + throw new ArgumentException("Content is null.", nameof(request)); + { - if (!_colorValidator.Validate(properties.Color, out var message)) - { - throw new ArgumentException(message, nameof(properties)); - } - properties.Time = properties.Time?.MyToUtc(); + if (!_colorValidator.Validate(request.Color, out var message)) + throw new ArgumentException("Color is not valid.", nameof(request)); } + request.Time = request.Time?.MyToUtc(); + await CheckTimelineExistence(timelineId); await CheckUserExistence(authorId); var currentTime = _clock.GetCurrentTime(); - var finalTime = properties?.Time ?? currentTime; + var finalTime = request.Time ?? currentTime; var postEntity = new TimelinePostEntity { @@ -307,10 +337,29 @@ namespace Timeline.Services TimelineId = timelineId, Time = finalTime, LastUpdated = currentTime, - Color = properties?.Color + Color = request.Color + }; + + switch (request.Content) + { + case TimelinePostCreateRequestTextContent content: + postEntity.ContentType = content.TypeName; + postEntity.Content = content.Text; + break; + case TimelinePostCreateRequestImageContent content: + var imageFormat = await _imageValidator.Validate(content.Data); + var imageFormatText = imageFormat.DefaultMimeType; + + var tag = await _dataManager.RetainEntry(content.Data); + + postEntity.ContentType = content.TypeName; + postEntity.Content = tag; + postEntity.ExtraContent = imageFormatText; + break; + default: + throw new ArgumentException("Unknown content type.", nameof(request)); }; - await saveContent(postEntity); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); timelineEntity.CurrentPostLocalId += 1; @@ -323,40 +372,6 @@ namespace Timeline.Services return postEntity; } - public async Task CreateTextPost(long timelineId, long authorId, string text, TimelinePostCommonProperties? properties = null) - { - if (text is null) - throw new ArgumentNullException(nameof(text)); - - return await GeneralCreatePost(timelineId, authorId, properties, (entity) => - { - entity.ContentType = TimelinePostContentTypes.Text; - entity.Content = text; - - return Task.CompletedTask; - }); - } - - public async Task CreateImagePost(long timelineId, long authorId, byte[] data, TimelinePostCommonProperties? properties = null) - { - if (data is null) - throw new ArgumentNullException(nameof(data)); - - await CheckTimelineExistence(timelineId); - - return await GeneralCreatePost(timelineId, authorId, properties, async (entity) => - { - var imageFormat = await _imageValidator.Validate(data); - var imageFormatText = imageFormat.DefaultMimeType; - - var tag = await _dataManager.RetainEntry(data); - - entity.ContentType = TimelinePostContentTypes.Image; - entity.Content = tag; - entity.ExtraContent = imageFormatText; - }); - } - public async Task DeletePost(long timelineId, long postId) { await CheckTimelineExistence(timelineId); -- cgit v1.2.3 From 8b7b570b008c9e9c8c0660c9650112d435269924 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 8 Feb 2021 19:12:44 +0800 Subject: ... --- BackEnd/Timeline/Services/TimelinePostService.cs | 178 ++++++++++++++++++----- 1 file changed, 140 insertions(+), 38 deletions(-) (limited to 'BackEnd/Timeline/Services/TimelinePostService.cs') diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index 076c45e8..66ec8090 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -31,27 +31,55 @@ namespace Timeline.Services public class TimelinePostCreateRequestTextContent : TimelinePostCreateRequestContent { + private string _text; + public TimelinePostCreateRequestTextContent(string text) { - Text = text; + if (text is null) + throw new ArgumentNullException(nameof(text)); + + _text = text; } public override string TypeName => TimelinePostContentTypes.Text; - public string Text { get; set; } + public string Text + { + get => _text; + set + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + _text = value; + } + } } public class TimelinePostCreateRequestImageContent : TimelinePostCreateRequestContent { + private byte[] _data; + public TimelinePostCreateRequestImageContent(byte[] data) { - Data = data; + if (data is null) + throw new ArgumentNullException(nameof(data)); + + _data = data; } public override string TypeName => TimelinePostContentTypes.Image; #pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } + public byte[] Data + { + get => _data; + set + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + _data = value; + } + } #pragma warning restore CA1819 // Properties should not return arrays } @@ -70,6 +98,13 @@ namespace Timeline.Services public TimelinePostCreateRequestContent Content { get; set; } } + public class TimelinePostPatchRequest + { + public string? Color { get; set; } + public DateTime? Time { get; set; } + public TimelinePostCreateRequestContent? Content { get; set; } + } + public interface ITimelinePostService { /// @@ -122,7 +157,7 @@ namespace Timeline.Services /// The id of the timeline to create post against. /// The author's user id. /// Info about the post. - /// The info of the created post. + /// The entity of the created post. /// Thrown when is null. /// Thrown when is of invalid format. /// Thrown when timeline does not exist. @@ -130,6 +165,20 @@ namespace Timeline.Services /// Thrown if data is not a image. Validated by . Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request); + /// + /// Modify a post. Change its properties or replace its content. + /// + /// The timeline id. + /// The post id. + /// The request. + /// The entity of the patched post. + /// Thrown when is null. + /// Thrown when is of invalid format. + /// Thrown when timeline does not exist. + /// Thrown when post does not exist. + /// Thrown if data is not a image. Validated by . + Task PatchPost(long timelineId, long postId, TimelinePostPatchRequest request); + /// /// Delete a post. /// @@ -309,6 +358,36 @@ namespace Timeline.Services }; } + private async Task SaveContent(TimelinePostEntity entity, TimelinePostCreateRequestContent content) + { + switch (content) + { + case TimelinePostCreateRequestTextContent c: + entity.ContentType = c.TypeName; + entity.Content = c.Text; + break; + case TimelinePostCreateRequestImageContent c: + var imageFormat = await _imageValidator.Validate(c.Data); + var imageFormatText = imageFormat.DefaultMimeType; + + var tag = await _dataManager.RetainEntry(c.Data); + + entity.ContentType = content.TypeName; + entity.Content = tag; + entity.ExtraContent = imageFormatText; + break; + default: + throw new ArgumentException("Unknown content type.", nameof(content)); + }; + } + + private async Task CleanContent(TimelinePostEntity entity) + { + if (entity.Content is not null && entity.ContentType == TimelinePostContentTypes.Image) + await _dataManager.FreeEntry(entity.Content); + entity.Content = null; + } + public async Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request) { if (request is null) @@ -331,6 +410,8 @@ namespace Timeline.Services var currentTime = _clock.GetCurrentTime(); var finalTime = request.Time ?? currentTime; + await using var transaction = await _database.Database.BeginTransactionAsync(); + var postEntity = new TimelinePostEntity { AuthorId = authorId, @@ -340,26 +421,7 @@ namespace Timeline.Services Color = request.Color }; - switch (request.Content) - { - case TimelinePostCreateRequestTextContent content: - postEntity.ContentType = content.TypeName; - postEntity.Content = content.Text; - break; - case TimelinePostCreateRequestImageContent content: - var imageFormat = await _imageValidator.Validate(content.Data); - var imageFormatText = imageFormat.DefaultMimeType; - - var tag = await _dataManager.RetainEntry(content.Data); - - postEntity.ContentType = content.TypeName; - postEntity.Content = tag; - postEntity.ExtraContent = imageFormatText; - break; - default: - throw new ArgumentException("Unknown content type.", nameof(request)); - }; - + await SaveContent(postEntity, request.Content); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); timelineEntity.CurrentPostLocalId += 1; @@ -369,37 +431,77 @@ namespace Timeline.Services await _database.SaveChangesAsync(); + await transaction.CommitAsync(); + return postEntity; } - public async Task DeletePost(long timelineId, long postId) + public async Task PatchPost(long timelineId, long postId, TimelinePostPatchRequest request) { + if (request is null) + throw new ArgumentNullException(nameof(request)); + + { + if (!_colorValidator.Validate(request.Color, out var message)) + throw new ArgumentException("Color is not valid.", nameof(request)); + } + + request.Time = request.Time?.MyToUtc(); + await CheckTimelineExistence(timelineId); - var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + var entity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); - if (post == null) + await using var transaction = await _database.Database.BeginTransactionAsync(); + + if (entity is null) throw new TimelinePostNotExistException(timelineId, postId, false); - if (post.Content == null) + if (entity.Content is null) throw new TimelinePostNotExistException(timelineId, postId, true); - string? dataTag = null; + if (request.Time.HasValue) + entity.Time = request.Time.Value; + + if (request.Color is not null) + entity.Color = request.Color; - if (post.ContentType == TimelinePostContentTypes.Image) + if (request.Content is not null) { - dataTag = post.Content; + await CleanContent(entity); + await SaveContent(entity, request.Content); } - post.Content = null; - post.LastUpdated = _clock.GetCurrentTime(); + entity.LastUpdated = _clock.GetCurrentTime(); await _database.SaveChangesAsync(); - if (dataTag != null) - { - await _dataManager.FreeEntry(dataTag); - } + await transaction.CommitAsync(); + + return entity; + } + + public async Task DeletePost(long timelineId, long postId) + { + await CheckTimelineExistence(timelineId); + + var entity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (entity == null) + throw new TimelinePostNotExistException(timelineId, postId, false); + + if (entity.Content == null) + throw new TimelinePostNotExistException(timelineId, postId, true); + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + await CleanContent(entity); + + entity.LastUpdated = _clock.GetCurrentTime(); + + await _database.SaveChangesAsync(); + + await transaction.CommitAsync(); } public async Task DeleteAllPostsOfUser(long userId) -- cgit v1.2.3