diff options
-rw-r--r-- | BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs | 65 | ||||
-rw-r--r-- | BackEnd/Timeline/Controllers/TimelinePostController.cs | 143 | ||||
-rw-r--r-- | BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs | 2 | ||||
-rw-r--r-- | BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs | 19 | ||||
-rw-r--r-- | BackEnd/Timeline/Models/Mapper/TimelineMapper.cs | 4 | ||||
-rw-r--r-- | BackEnd/Timeline/Models/TimelinePostContentTypes.cs | 14 | ||||
-rw-r--r-- | BackEnd/Timeline/Models/TimelineVisibility.cs (renamed from BackEnd/Timeline/Models/Timeline.cs) | 8 | ||||
-rw-r--r-- | BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs | 18 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/TimelinePostService.cs | 271 |
9 files changed, 432 insertions, 112 deletions
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs index f05ed7af..17c85f22 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs @@ -491,6 +491,8 @@ namespace Timeline.Tests.IntegratedTests Color = "#1"
});
+ long id;
+
{
var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest
{
@@ -498,7 +500,70 @@ namespace Timeline.Tests.IntegratedTests Color = "#aabbcc"
});
post.Color.Should().Be("#aabbcc");
+ id = post.Id;
+ }
+
+ {
+ var post = await client.TestGetAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{id}");
+ post.Color.Should().Be("#aabbcc");
}
}
+
+ [Theory]
+ [MemberData(nameof(TimelineNameGeneratorTestData))]
+ public async Task GetPost(TimelineNameGenerator generator)
+ {
+ using var client = await CreateClientAsUser();
+
+ HttpTimelinePostCreateRequestContent CreateRequestContent() => new()
+ {
+ Type = "text",
+ Text = "aaa"
+ };
+
+ await client.TestGetAssertNotFoundAsync($"timelines/{generator(1)}/posts/1");
+
+ var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest
+ {
+ Content = CreateRequestContent(),
+ });
+
+ var post2 = await client.TestGetAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{post.Id}");
+ post2.Should().BeEquivalentTo(post);
+
+ await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{post.Id}");
+
+ await client.TestGetAssertNotFoundAsync($"timelines/{generator(1)}/posts/{post.Id}");
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineNameGeneratorTestData))]
+ public async Task PatchPost(TimelineNameGenerator generator)
+ {
+ using var client = await CreateClientAsUser();
+
+ var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest
+ {
+ Content = new()
+ {
+ Type = "text",
+ Text = "aaa"
+ }
+ });
+
+ var date = new DateTime(2000, 10, 1);
+
+ var post2 = await client.TestPatchAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{post.Id}", new HttpTimelinePostPatchRequest
+ {
+ Time = date,
+ Color = "#aabbcc"
+ });
+ post2.Time.Should().Be(date);
+ post2.Color.Should().Be("#aabbcc");
+
+ var post3 = await client.TestGetAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{post.Id}");
+ post3.Time.Should().Be(date);
+ post3.Color.Should().Be("#aabbcc");
+ }
}
}
diff --git a/BackEnd/Timeline/Controllers/TimelinePostController.cs b/BackEnd/Timeline/Controllers/TimelinePostController.cs index 3f31decf..44498c58 100644 --- a/BackEnd/Timeline/Controllers/TimelinePostController.cs +++ b/BackEnd/Timeline/Controllers/TimelinePostController.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Mvc; using System;
using System.Collections.Generic;
using System.Threading.Tasks;
-using Timeline.Entities;
using Timeline.Filters;
using Timeline.Helpers;
using Timeline.Models;
@@ -53,7 +52,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<List<HttpTimelinePost>>> PostList([FromRoute][GeneralTimelineName] string timeline, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted)
+ public async Task<ActionResult<List<HttpTimelinePost>>> List([FromRoute][GeneralTimelineName] string timeline, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted)
{
var timelineId = await _timelineService.GetTimelineIdByName(timeline);
@@ -69,6 +68,37 @@ namespace Timeline.Controllers }
/// <summary>
+ /// Get a post of a timeline.
+ /// </summary>
+ /// <param name="timeline">The name of the timeline.</param>
+ /// <param name="postId">The post id.</param>
+ /// <returns>The post.</returns>
+ [HttpGet("{post}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<HttpTimelinePost>> Get([FromRoute][GeneralTimelineName] string timeline, [FromRoute(Name = "post")] long postId)
+ {
+ var timelineId = await _timelineService.GetTimelineIdByName(timeline);
+
+ if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermission(timelineId, this.GetOptionalUserId()))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ try
+ {
+ var post = await _postService.GetPost(timelineId, postId);
+ var result = await _timelineMapper.MapToHttp(post, timeline, Url);
+ return result;
+ }
+ catch (TimelinePostNotExistException)
+ {
+ return NotFound(ErrorResponse.TimelineController.PostNotExist());
+ }
+ }
+
+ /// <summary>
/// Get the data of a post. Usually a image post.
/// </summary>
/// <param name="timeline">Timeline name.</param>
@@ -82,7 +112,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<IActionResult> PostDataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch)
+ public async Task<IActionResult> DataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch)
{
_ = ifNoneMatch;
@@ -121,7 +151,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult<HttpTimelinePost>> PostPost([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body)
+ public async Task<ActionResult<HttpTimelinePost>> Post([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body)
{
var timelineId = await _timelineService.GetTimelineIdByName(timeline);
var userId = this.GetUserId();
@@ -131,54 +161,85 @@ 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 };
+ switch (requestContent.Type)
+ {
+ 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));
- if (content.Type == TimelinePostContentTypes.Text)
+ // 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));
+
+ }
+
+ try
{
- 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);
+ 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 if (content.Type == TimelinePostContentTypes.Image)
+ catch (ImageException)
{
- 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));
- }
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage));
+ }
+ }
- try
- {
- post = await _postService.CreateImagePost(timelineId, userId, data, properties);
- }
- catch (ImageException)
+ /// <summary>
+ /// Update a post except content.
+ /// </summary>
+ /// <param name="timeline">Timeline name.</param>
+ /// <param name="post">Post id.</param>
+ /// <param name="body">Request body.</param>
+ /// <returns>New info of post.</returns>
+ [HttpPatch("{post}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<HttpTimelinePost>> Patch([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromBody] HttpTimelinePostPatchRequest body)
+ {
+ var timelineId = await _timelineService.GetTimelineIdByName(timeline);
+
+ try
+ {
+ if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true))
{
- return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage));
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
+
+ var entity = await _postService.PatchPost(timelineId, post, new TimelinePostPatchRequest { Time = body.Time, Color = body.Color });
+ var result = await _timelineMapper.MapToHttp(entity, timeline, Url);
+ return Ok(result);
}
- else
+ catch (TimelinePostNotExistException)
{
- return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType));
+ return BadRequest(ErrorResponse.TimelineController.PostNotExist());
}
-
- var result = await _timelineMapper.MapToHttp(post, timeline, Url);
- return result;
}
/// <summary>
@@ -193,7 +254,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> PostDelete([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post)
+ public async Task<ActionResult> Delete([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post)
{
var timelineId = await _timelineService.GetTimelineIdByName(timeline);
diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs index f4b300a9..12ab407f 100644 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePostCreateRequestContent.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations;
+using Timeline.Models.Validation;
namespace Timeline.Models.Http
{
@@ -11,6 +12,7 @@ namespace Timeline.Models.Http /// Type of post content.
/// </summary>
[Required]
+ [TimelinePostContentType]
public string Type { get; set; } = default!;
/// <summary>
/// If post is of text type, this is the text.
diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs new file mode 100644 index 00000000..2c6edf66 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs @@ -0,0 +1,19 @@ +using System;
+using Timeline.Models.Validation;
+
+namespace Timeline.Models.Http
+{
+ public class HttpTimelinePostPatchRequest
+ {
+ /// <summary>
+ /// Change the time. Null for not change.
+ /// </summary>
+ public DateTime? Time { get; set; }
+
+ /// <summary>
+ /// Change the color. Null for not change.
+ /// </summary>
+ [Color]
+ public string? Color { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs index 94e55237..88c96d8a 100644 --- a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs +++ b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs @@ -49,7 +49,7 @@ namespace Timeline.Models.Mapper isBookmark: userId is not null && await _bookmarkTimelineService.IsBookmark(userId.Value, entity.Id, false, false),
links: new HttpTimelineLinks(
self: urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { timeline = timelineName }),
- posts: urlHelper.ActionLink(nameof(TimelinePostController.PostList), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName })
+ posts: urlHelper.ActionLink(nameof(TimelinePostController.List), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName })
)
);
}
@@ -84,7 +84,7 @@ namespace Timeline.Models.Mapper (
type: TimelinePostContentTypes.Image,
text: null,
- url: urlHelper.ActionLink(nameof(TimelinePostController.PostDataGet), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName, post = entity.LocalId }),
+ url: urlHelper.ActionLink(nameof(TimelinePostController.DataGet), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName, post = entity.LocalId }),
eTag: $"\"{entity.Content}\""
),
_ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, "Unknown timeline post type {0}.", entity.ContentType))
diff --git a/BackEnd/Timeline/Models/TimelinePostContentTypes.cs b/BackEnd/Timeline/Models/TimelinePostContentTypes.cs new file mode 100644 index 00000000..ca5e79e1 --- /dev/null +++ b/BackEnd/Timeline/Models/TimelinePostContentTypes.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic;
+
+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/Models/Timeline.cs b/BackEnd/Timeline/Models/TimelineVisibility.cs index 9f3eabdf..7c1e309b 100644 --- a/BackEnd/Timeline/Models/Timeline.cs +++ b/BackEnd/Timeline/Models/TimelineVisibility.cs @@ -1,4 +1,4 @@ -namespace Timeline.Models
+namespace Timeline.Models
{
public enum TimelineVisibility
{
@@ -15,10 +15,4 @@ /// </summary>
Private
}
-
- public static class TimelinePostContentTypes
- {
- public const string Text = "text";
- public const string Image = "image";
- }
}
diff --git a/BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs b/BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs new file mode 100644 index 00000000..483cce06 --- /dev/null +++ b/BackEnd/Timeline/Models/Validation/TimelinePostContentTypeValidator.cs @@ -0,0 +1,18 @@ +using System;
+
+namespace Timeline.Models.Validation
+{
+ public class TimelinePostContentTypeValidator : StringSetValidator
+ {
+ public TimelinePostContentTypeValidator() : base(TimelinePostContentTypes.AllTypes) { }
+ }
+
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
+ public class TimelinePostContentTypeAttribute : ValidateWithAttribute
+ {
+ public TimelinePostContentTypeAttribute() : base(typeof(TimelinePostContentTypeValidator))
+ {
+
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index c2b773ff..66ec8090 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -24,12 +24,85 @@ 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
+ {
+ private string _text;
+
+ public TimelinePostCreateRequestTextContent(string text)
+ {
+ if (text is null)
+ throw new ArgumentNullException(nameof(text));
+
+ _text = text;
+ }
+
+ public override string TypeName => TimelinePostContentTypes.Text;
+
+ 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)
+ {
+ 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 => _data;
+ set
+ {
+ if (value is null)
+ throw new ArgumentNullException(nameof(value));
+ _data = value;
+ }
+ }
+#pragma warning restore CA1819 // Properties should not return arrays
+ }
+
+ public class TimelinePostCreateRequest
+ {
+ public TimelinePostCreateRequest(TimelinePostCreateRequestContent content)
+ {
+ Content = content;
+ }
+
public string? Color { get; set; }
/// <summary>If not set, current time is used.</summary>
public DateTime? Time { get; set; }
+
+ 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
@@ -45,6 +118,17 @@ namespace Timeline.Services Task<List<TimelinePostEntity>> GetPosts(long timelineId, DateTime? modifiedSince = null, bool includeDeleted = false);
/// <summary>
+ /// Get a post of a timeline.
+ /// </summary>
+ /// <param name="timelineId">The id of the timeline of the post.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <param name="includeDelete">If true, return the entity even if it is deleted.</param>
+ /// <returns>The post.</returns>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
+ Task<TimelinePostEntity> GetPost(long timelineId, long postId, bool includeDelete = false);
+
+ /// <summary>
/// Get the etag of data of a post.
/// </summary>
/// <param name="timelineId">The id of the timeline of the post.</param>
@@ -68,31 +152,32 @@ namespace Timeline.Services Task<PostData> GetPostData(long timelineId, long postId);
/// <summary>
- /// Create a new text post in timeline.
+ /// Create a new post in timeline.
/// </summary>
/// <param name="timelineId">The id of the timeline to create post against.</param>
/// <param name="authorId">The author's user id.</param>
- /// <param name="text">The content text.</param>
- /// <param name="properties">Some properties.</param>
- /// <returns>The info of the created post.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="text"/> is null.</exception>
+ /// <param name="request">Info about the post.</param>
+ /// <returns>The entity of the created post.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="request"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="request"/> is of invalid format.</exception>
/// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
/// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
- Task<TimelinePostEntity> CreateTextPost(long timelineId, long authorId, string text, TimelinePostCommonProperties? properties = null);
+ /// <exception cref="ImageException">Thrown if data is not a image. Validated by <see cref="ImageValidator"/>.</exception>
+ Task<TimelinePostEntity> CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request);
/// <summary>
- /// Create a new image post in timeline.
+ /// Modify a post. Change its properties or replace its content.
/// </summary>
- /// <param name="timelineId">The id of the timeline to create post against.</param>
- /// <param name="authorId">The author's user id.</param>
- /// <param name="imageData">The image data.</param>
- /// <param name="properties">Some properties.</param>
- /// <returns>The info of the created post.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="imageData"/> is null.</exception>
+ /// <param name="timelineId">The timeline id.</param>
+ /// <param name="postId">The post id.</param>
+ /// <param name="request">The request.</param>
+ /// <returns>The entity of the patched post.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="request"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="request"/> is of invalid format.</exception>
/// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
- /// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when post does not exist.</exception>
/// <exception cref="ImageException">Thrown if data is not a image. Validated by <see cref="ImageValidator"/>.</exception>
- Task<TimelinePostEntity> CreateImagePost(long timelineId, long authorId, byte[] imageData, TimelinePostCommonProperties? properties = null);
+ Task<TimelinePostEntity> PatchPost(long timelineId, long postId, TimelinePostPatchRequest request);
/// <summary>
/// Delete a post.
@@ -189,6 +274,25 @@ namespace Timeline.Services return await query.ToListAsync();
}
+ public async Task<TimelinePostEntity> 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<string> GetPostDataETag(long timelineId, long postId)
{
await CheckTimelineExistence(timelineId);
@@ -254,22 +358,59 @@ namespace Timeline.Services };
}
- private async Task<TimelinePostEntity> GeneralCreatePost(long timelineId, long authorId, TimelinePostCommonProperties? properties, Func<TimelinePostEntity, Task> saveContent)
+ private async Task SaveContent(TimelinePostEntity entity, TimelinePostCreateRequestContent content)
{
- if (properties is not null)
+ switch (content)
{
- if (!_colorValidator.Validate(properties.Color, out var message))
- {
- throw new ArgumentException(message, nameof(properties));
- }
- properties.Time = properties.Time?.MyToUtc();
+ 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<TimelinePostEntity> CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request)
+ {
+ 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(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;
+
+ await using var transaction = await _database.Database.BeginTransactionAsync();
var postEntity = new TimelinePostEntity
{
@@ -277,10 +418,10 @@ namespace Timeline.Services TimelineId = timelineId,
Time = finalTime,
LastUpdated = currentTime,
- Color = properties?.Color
+ Color = request.Color
};
- await saveContent(postEntity);
+ await SaveContent(postEntity, request.Content);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
timelineEntity.CurrentPostLocalId += 1;
@@ -290,71 +431,77 @@ namespace Timeline.Services await _database.SaveChangesAsync();
+ await transaction.CommitAsync();
+
return postEntity;
}
- public async Task<TimelinePostEntity> CreateTextPost(long timelineId, long authorId, string text, TimelinePostCommonProperties? properties = null)
+ public async Task<TimelinePostEntity> PatchPost(long timelineId, long postId, TimelinePostPatchRequest request)
{
- if (text is null)
- throw new ArgumentNullException(nameof(text));
+ if (request is null)
+ throw new ArgumentNullException(nameof(request));
- return await GeneralCreatePost(timelineId, authorId, properties, (entity) =>
{
- entity.ContentType = TimelinePostContentTypes.Text;
- entity.Content = text;
-
- return Task.CompletedTask;
- });
- }
+ if (!_colorValidator.Validate(request.Color, out var message))
+ throw new ArgumentException("Color is not valid.", nameof(request));
+ }
- public async Task<TimelinePostEntity> CreateImagePost(long timelineId, long authorId, byte[] data, TimelinePostCommonProperties? properties = null)
- {
- if (data is null)
- throw new ArgumentNullException(nameof(data));
+ request.Time = request.Time?.MyToUtc();
await CheckTimelineExistence(timelineId);
- return await GeneralCreatePost(timelineId, authorId, properties, async (entity) =>
+ var entity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
+
+ await using var transaction = await _database.Database.BeginTransactionAsync();
+
+ if (entity is null)
+ throw new TimelinePostNotExistException(timelineId, postId, false);
+
+ if (entity.Content is null)
+ throw new TimelinePostNotExistException(timelineId, postId, true);
+
+ if (request.Time.HasValue)
+ entity.Time = request.Time.Value;
+
+ if (request.Color is not null)
+ entity.Color = request.Color;
+
+ if (request.Content is not null)
{
- var imageFormat = await _imageValidator.Validate(data);
- var imageFormatText = imageFormat.DefaultMimeType;
+ await CleanContent(entity);
+ await SaveContent(entity, request.Content);
+ }
+
+ entity.LastUpdated = _clock.GetCurrentTime();
+
+ await _database.SaveChangesAsync();
- var tag = await _dataManager.RetainEntry(data);
+ await transaction.CommitAsync();
- entity.ContentType = TimelinePostContentTypes.Image;
- entity.Content = tag;
- entity.ExtraContent = imageFormatText;
- });
+ return entity;
}
public async Task DeletePost(long timelineId, long postId)
{
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)
+ if (entity == null)
throw new TimelinePostNotExistException(timelineId, postId, false);
- if (post.Content == null)
+ if (entity.Content == null)
throw new TimelinePostNotExistException(timelineId, postId, true);
- string? dataTag = null;
+ await using var transaction = await _database.Database.BeginTransactionAsync();
- if (post.ContentType == TimelinePostContentTypes.Image)
- {
- dataTag = post.Content;
- }
+ await CleanContent(entity);
- post.Content = null;
- post.LastUpdated = _clock.GetCurrentTime();
+ entity.LastUpdated = _clock.GetCurrentTime();
await _database.SaveChangesAsync();
- if (dataTag != null)
- {
- await _dataManager.FreeEntry(dataTag);
- }
+ await transaction.CommitAsync();
}
public async Task DeleteAllPostsOfUser(long userId)
|