aboutsummaryrefslogtreecommitdiff
path: root/BackEnd/Timeline
diff options
context:
space:
mode:
Diffstat (limited to 'BackEnd/Timeline')
-rw-r--r--BackEnd/Timeline/Controllers/SearchController.cs61
-rw-r--r--BackEnd/Timeline/Controllers/TimelineController.cs2
-rw-r--r--BackEnd/Timeline/Models/Http/TimelineController.cs2
-rw-r--r--BackEnd/Timeline/Services/SearchService.cs104
-rw-r--r--BackEnd/Timeline/Startup.cs2
5 files changed, 169 insertions, 2 deletions
diff --git a/BackEnd/Timeline/Controllers/SearchController.cs b/BackEnd/Timeline/Controllers/SearchController.cs
new file mode 100644
index 00000000..dec876b6
--- /dev/null
+++ b/BackEnd/Timeline/Controllers/SearchController.cs
@@ -0,0 +1,61 @@
+using Microsoft.AspNetCore.Mvc;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Models.Http;
+using Timeline.Models.Mapper;
+using Timeline.Services;
+
+namespace Timeline.Controllers
+{
+ /// <summary>
+ /// Api related to search timelines or users.
+ /// </summary>
+ [ApiController]
+ [ProducesErrorResponseType(typeof(CommonResponse))]
+ [Route("search")]
+ public class SearchController : Controller
+ {
+ private readonly ISearchService _service;
+ private readonly TimelineMapper _timelineMapper;
+ private readonly UserMapper _userMapper;
+
+ public SearchController(ISearchService service, TimelineMapper timelineMapper, UserMapper userMapper)
+ {
+ _service = service;
+ _timelineMapper = timelineMapper;
+ _userMapper = userMapper;
+ }
+
+ /// <summary>
+ /// Search timelines whose name or title contains query string case-insensitively.
+ /// </summary>
+ /// <param name="query">The string to contain.</param>
+ /// <returns>Timelines with most related at first.</returns>
+ [HttpGet("timelines")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ public async Task<ActionResult<List<HttpTimeline>>> TimelineSearch([FromQuery(Name = "q"), Required(AllowEmptyStrings = false)] string query)
+ {
+ var searchResult = await _service.SearchTimeline(query);
+ var timelines = searchResult.Items.Select(i => i.Item).ToList();
+ return await _timelineMapper.MapToHttp(timelines, Url, this.GetOptionalUserId());
+ }
+
+ /// <summary>
+ /// Search users whose username or nick contains query string case-insensitively.
+ /// </summary>
+ /// <param name="query">The string to contain.</param>
+ /// <returns>Users with most related at first.</returns>
+ [HttpGet("users")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ public async Task<ActionResult<List<HttpUser>>> UserSearch([FromQuery(Name = "q"), Required(AllowEmptyStrings = false)] string query)
+ {
+ var searchResult = await _service.SearchUser(query);
+ var users = searchResult.Items.Select(i => i.Item).ToList();
+ return await _userMapper.MapToHttp(users, Url);
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs
index b2e37b15..5d484388 100644
--- a/BackEnd/Timeline/Controllers/TimelineController.cs
+++ b/BackEnd/Timeline/Controllers/TimelineController.cs
@@ -441,7 +441,7 @@ namespace Timeline.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
- public async Task<ActionResult<HttpTimeline>> TimelineCreate([FromBody] TimelineCreateRequest body)
+ public async Task<ActionResult<HttpTimeline>> TimelineCreate([FromBody] HttpTimelineCreateRequest body)
{
var userId = this.GetUserId();
diff --git a/BackEnd/Timeline/Models/Http/TimelineController.cs b/BackEnd/Timeline/Models/Http/TimelineController.cs
index f6039b35..257076f0 100644
--- a/BackEnd/Timeline/Models/Http/TimelineController.cs
+++ b/BackEnd/Timeline/Models/Http/TimelineController.cs
@@ -43,7 +43,7 @@ namespace Timeline.Models.Http
/// <summary>
/// Create timeline request model.
/// </summary>
- public class TimelineCreateRequest
+ public class HttpTimelineCreateRequest
{
/// <summary>
/// Name of the new timeline. Must be a valid name.
diff --git a/BackEnd/Timeline/Services/SearchService.cs b/BackEnd/Timeline/Services/SearchService.cs
new file mode 100644
index 00000000..680ef9e3
--- /dev/null
+++ b/BackEnd/Timeline/Services/SearchService.cs
@@ -0,0 +1,104 @@
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+
+namespace Timeline.Services
+{
+ public class SearchResultItem<TItem>
+ {
+ public SearchResultItem(TItem item, int score)
+ {
+ Item = item;
+ Score = score;
+ }
+
+ public TItem Item { get; set; } = default!;
+
+ /// <summary>
+ /// Bigger is better.
+ /// </summary>
+ public int Score { get; set; }
+ }
+
+ public class SearchResult<TItem>
+ {
+#pragma warning disable CA2227 // Collection properties should be read only
+ public List<SearchResultItem<TItem>> Items { get; set; } = new();
+#pragma warning restore CA2227 // Collection properties should be read only
+ }
+
+ public interface ISearchService
+ {
+ /// <summary>
+ /// Search timelines whose name or title contains query string.
+ /// </summary>
+ /// <param name="query">String to contain.</param>
+ /// <returns>Search results.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="query"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="query"/> is empty.</exception>
+ /// <remarks>
+ /// Implementation should promise high score is at first.
+ /// </remarks>
+ Task<SearchResult<TimelineEntity>> SearchTimeline(string query);
+
+ /// <summary>
+ /// Search users whose username or nickname contains query string.
+ /// </summary>
+ /// <param name="query">String to contain.</param>
+ /// <returns>Search results.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="query"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="query"/> is empty.</exception>
+ /// <remarks>
+ /// Implementation should promise high score is at first.
+ /// </remarks>
+ Task<SearchResult<UserEntity>> SearchUser(string query);
+ }
+
+ public class SearchService : ISearchService
+ {
+ private readonly DatabaseContext _database;
+
+ public SearchService(DatabaseContext database)
+ {
+ _database = database;
+ }
+
+ public async Task<SearchResult<TimelineEntity>> SearchTimeline(string query)
+ {
+ if (query is null)
+ throw new ArgumentNullException(nameof(query));
+ if (query.Length == 0)
+ throw new ArgumentException("Query string can't be empty.", nameof(query));
+
+ var nameLikeTimelines = await _database.Timelines.Include(t => t.Owner).Where(t => t.Name == null ? t.Owner.Username.Contains(query) : t.Name.Contains(query)).ToListAsync();
+ var titleLikeTimelines = await _database.Timelines.Where(t => t.Title != null && t.Title.Contains(query)).ToListAsync();
+
+ var searchResult = new SearchResult<TimelineEntity>();
+ searchResult.Items.AddRange(nameLikeTimelines.Select(t => new SearchResultItem<TimelineEntity>(t, 2)));
+ searchResult.Items.AddRange(titleLikeTimelines.Select(t => new SearchResultItem<TimelineEntity>(t, 1)));
+
+ return searchResult;
+ }
+
+ public async Task<SearchResult<UserEntity>> SearchUser(string query)
+ {
+ if (query is null)
+ throw new ArgumentNullException(nameof(query));
+ if (query.Length == 0)
+ throw new ArgumentException("Query string can't be empty.", nameof(query));
+
+ var usernameLikeUsers = await _database.Users.Where(u => u.Username.Contains(query)).ToListAsync();
+ var nicknameLikeUsers = await _database.Users.Where(u => u.Nickname != null && u.Nickname.Contains(query)).ToListAsync();
+
+ var searchResult = new SearchResult<UserEntity>();
+ searchResult.Items.AddRange(usernameLikeUsers.Select(u => new SearchResultItem<UserEntity>(u, 2)));
+ searchResult.Items.AddRange(nicknameLikeUsers.Select(u => new SearchResultItem<UserEntity>(u, 1)));
+
+ return searchResult;
+
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs
index cb99c138..0fab798b 100644
--- a/BackEnd/Timeline/Startup.cs
+++ b/BackEnd/Timeline/Startup.cs
@@ -120,6 +120,8 @@ namespace Timeline
services.AddScoped<IHighlightTimelineService, HighlightTimelineService>();
services.AddScoped<IBookmarkTimelineService, BookmarkTimelineService>();
+ services.AddScoped<ISearchService, SearchService>();
+
services.AddOpenApiDocs();
if (_frontEndMode == FrontEndMode.Mock)